39 votes

Quels détails d'implémentation font que ce code échoue si facilement?

Cette question n'est pas sur le bien-connu et documenté fait qu' HashMap n'est pas thread-safe, mais ses modes de défaillance sur HotSpot et JDK code. Je suis surpris par la façon dont facilement ce code ne fonctionne pas avec un NPE:

public static void main(String[] args) {
    Map<Integer, Integer> m = new HashMap<>(0, 0.75f);
    IntStream.range(0, 5).parallel().peek(i -> m.put(i, i)).map(m::get).count();
}

Il n'y a pas de mystère, où les entrées en phase nationale vient de: en .map(m::get) étape, tout en essayant de unbox un null. Il échoue dans environ 4 sur 5 fonctionne.

Sur ma machine, Runtime#availableProcessors() 8 rapports, donc vraisemblablement la gamme de longueur 5 est divisé en 5 sous-tâches, chacune avec un seul membre. J'ai aussi assumer mon code s'exécute en mode interprété. Il pourrait être l'appel JIT-compilé HashMap ou Stream méthodes, mais le haut niveau est interprété, donc excluant les variations où HashMap etat est chargé dans le thread de la mémoire locale (registres, pile), retardant ainsi l'observation des mises à jour par un autre thread. Si certains des cinq put opérations ne pas exécuter littéralement, pendant le même temps sur les différents cœurs, je ne vous attendez pas à détruire l' HashMaps structure interne. Le calendrier de chacune des tâches doit être extrêmement précise, étant donné le peu de quantité de travail.

Est-ce vraiment le moment précis (commonPool's fils doit être unparked), ou est-il une autre route à cause de cette panne sur Oracle/OpenJDK HotSpot? Ma version actuelle est

java version "1.8.0_72"
Java(TM) SE Runtime Environment (build 1.8.0_72-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.72-b15, mixed mode)

Mise à JOUR: je trouve que même la prise de deux insertions a un même taux d'échec élevé:

IntStream.range(0, 2).parallel().peek(i -> m.put(i, i)).map(m::get).count();

42voto

Holger Points 13789

Tout d'abord, il ne manque pas de manière fiable. J'ai réussi à avoir quelques pistes où aucune exception s'est produite. Ceci, cependant, ne signifie pas que la carte est correcte. Il est également possible que chaque thread témoins de sa propre valeur avec succès, tandis que la carte qui manque plusieurs mappages.

Mais en effet, à défaut avec une NullPointerException arrive assez souvent. J'ai créé le code de débogage pour illustrer l' HashMapde travail de:

static <K,V> void debugPut(HashMap<K,V> m, K k, V v) {
    if(m.isEmpty()) debug(m);
    m.put(k, v);
    debug(m);
}
private static <K, V> void debug(HashMap<K, V> m) {
    for(Field f: FIELDS) try {
        System.out.println(f.getName()+": "+f.get(m));
    } catch(ReflectiveOperationException ex) {
        throw new AssertionError(ex);
    }
    System.out.println();
}
static final Field[] FIELDS;
static {
    String[] name={ "table", "size", "threshold" };
    Field[] f=new Field[name.length];
    for (int ix = 0; ix < name.length; ix++) try {
        f[ix]=HashMap.class.getDeclaredField(name[ix]);
    }
    catch (NoSuchFieldException ex) {
        throw new ExceptionInInitializerError(ex);
    }
    AccessibleObject.setAccessible(f, true);
    FIELDS=f;
}

L'utilisation de ce avec la simple séquentielle for(int i=0; i<5; i++) debugPut(m, i, i); imprimé:

table: null
size: 0
threshold: 1

table: [Ljava.util.HashMap$Node;@70dea4e
size: 1
threshold: 1

table: [Ljava.util.HashMap$Node;@5c647e05
size: 2
threshold: 3

table: [Ljava.util.HashMap$Node;@5c647e05
size: 3
threshold: 3

table: [Ljava.util.HashMap$Node;@33909752
size: 4
threshold: 6

table: [Ljava.util.HashMap$Node;@33909752
size: 5
threshold: 6

Comme vous pouvez le voir, en raison de la capacité initiale de l' 0, il existe trois types de sauvegarde des tableaux créés, même pendant le fonctionnement séquentiel. À chaque fois, la capacité est augmentée, il ya une chance plus élevée que racé, d'un simultanées put manque le tableau de mise à jour et crée son propre tableau.

Cela est particulièrement pertinent pour l'état initial d'une carte vide et plusieurs threads tentent de mettre leur première clé, comme tous les threads peut rencontrer l'état initial d'un null tableau et de créer leurs propres. Aussi, même lors de la lecture de l'état d'un premier put, il y a un nouveau tableau créé pour le deuxième put ainsi.

Mais l'étape-par-étape de débogage révélé encore plus de chances de rupture:

À l'intérieur de la méthode de putVal, nous voyons à la fin:

++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

En d'autres termes, après la réussite de l'insertion d'une nouvelle clé, le tableau sera redimensionnée, si la nouvelle taille dépasse l' threshold. Ainsi, sur la première put, resize() est appelé au début, parce que le tableau est null et depuis votre capacité initiale est de 0, c'est à dire trop faible pour stocker une cartographie, de la nouvelle capacité sera 1 et la nouvelle - threshold sera 1 * loadFactor == 1 * 0.75f == 0.75f, arrondi à l' 0. Donc, à la fin de la première put, la nouvelle - threshold est dépassé et un autre resize() opération déclenchée. Donc, avec une capacité initiale de 0, la première put déjà crée et remplit deux tableaux, ce qui donne beaucoup plus de chances de briser, si plusieurs threads d'exécuter cette action en parallèle, tout en rencontrant l'état initial.

Et il y a un autre point. La recherche dans l' resize() de l'opération , nous voyons les lignes:

 @SuppressWarnings({"rawtypes","unchecked"})
 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 table = newTab;
 if (oldTab != null) {
     … (transfer old contents to new array)

En d'autres termes, le nouveau tableau de référence est stockée dans le tas avant , il a été rempli avec les anciennes entrées, de sorte que même sans réorganisation de lecture et d'écriture, il y a une chance qu'un autre thread qui lit de référence sans voir les anciennes entrées, y compris celui qu'il a écrit lui-même auparavant. En fait, les optimisations de réduire le tas d'accès peuvent réduire les chances d'un fil de ne pas voir sa propre mise à jour dans une immédiatement après la requête.

Encore, il convient également de noter que l'hypothèse que tout fonctionne interprétée ici, n'est pas fondé. Depuis HashMap est utilisé par le JRE aussi bien à l'interne, avant même de votre application démarre, il y a aussi une chance de rencontrer déjà le code compilé lors de l'utilisation d' HashMap.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X