web-dev-qa-db-fra.com

Comprendre le code de la méthode de calcul ConcurrentHashMap

Je viens de trouver cet étrange code dans la méthode de calcul ConcurrentHashMap: (ligne 1847)

public V compute(K key,
                 BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    ...
    Node<K,V> r = new ReservationNode<K,V>();
    synchronized (r) {   <--- what is this?
        if (casTabAt(tab, i, null, r)) {
            binCount = 1;
            Node<K,V> node = null;

Ainsi, le code effectue la synchronisation sur une nouvelle variable qui n'est disponible que pour le thread actuel. Cela signifie qu'il n'y a pas d'autre thread pour rivaliser pour ce verrou ou pour provoquer des effets de barrages de mémoire.

Quel est l'intérêt de cette action? Est-ce une erreur ou cela provoque des effets secondaires non évidents dont je ne suis pas au courant?

p.s. jdk1.8.0_131

46
AdamSkywalker
casTabAt(tab, i, null, r)

publie la référence à r.

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

Étant donné que c est placé dans tab, il est possible qu'il soit accessible par un autre thread, par exemple dans putVal . En tant que tel, ce bloc synchronized est nécessaire pour empêcher d'autres threads de faire d'autres choses synchronisées avec ce Node.

39
Andy Turner

Alors que r est une nouvelle variable à ce stade, elle est placée dans le table interne immédiatement par le biais de if (casTabAt(tab, i, null, r)) auquel point un autre thread peut y accéder dans différentes parties de le code.

Un commentaire interne non javadoc le décrit ainsi

L'insertion (via put ou ses variantes) du premier nœud dans un bac vide est effectuée en le plaçant simplement dans le bac. C'est de loin le cas le plus courant pour les opérations de placement sous la plupart des distributions de clés/hachage. D'autres opérations de mise à jour (insertion, suppression et remplacement) nécessitent des verrous. Nous ne voulons pas gaspiller l'espace requis pour associer un objet de verrouillage distinct à chaque casier, alors utilisez plutôt le premier nœud d'une liste de casiers lui-même comme verrou. La prise en charge du verrouillage de ces verrous repose sur des moniteurs "synchronisés" intégrés.

16
Kayaman

Juste 0,02 $ ici

Ce que vous avez montré là est en fait juste le ReservationNode - ce qui signifie que le bac est vide et qu'un réservation de certains Node est fait. Notez que cette méthode remplace plus tard ceci Node avec un vrai:

 setTabAt(tab, i, node);

Donc, cela est fait pour que le remplacement soit atomique pour autant que je sache. Une fois publié via casTabAt et si d'autres threads le voient - ils ne peuvent pas se synchroniser dessus car le verrou est déjà maintenu.

Notez également que lorsqu'il y a une entrée dans un bac, ce premier Node est utilisé pour se synchroniser (il est plus bas dans la méthode):

boolean added = false;
            synchronized (f) { // locks the bin on the first Node
                if (tabAt(tab, i) == f) {
......

En tant que nœud latéral, cette méthode a changé dans 9, puisque 8. Par exemple, exécuter ce code:

 map.computeIfAbsent("KEY", s -> {
    map.computeIfAbsent("KEY"), s -> {
        return 2;
    }
 })

ne finirait jamais en 8, mais jetterait un Recursive Update en 9.

3
Eugene