web-dev-qa-db-fra.com

Java ConcurrentHashMap.computeIfPresent visibilité de la modification de valeur

Disons que j'ai une carte concurrente avec des collections comme valeur:

Map<Integer, List<Integer> map = new ConcurrentHashMap<>();
map.putIfAbsent(8, new ArrayList<>());

et je mets à jour la valeur comme suit:

map.computeIfPresent(8, (i, c) -> {
    c.add(5);
    return c;
});

Je sais que computeIfPresent toute l'invocation de méthode est effectuée de manière atomique. Cependant, étant donné que plusieurs threads accèdent simultanément à cette mappe, la visibilité des données sur les modifications apportées à la collection sous-jacente me préoccupe un peu. Dans ce cas, la valeur 5 apparaîtra-t-elle dans la liste après avoir appelé map.get

Ma question est: la liste sera-t-elle visible dans les autres threads lors de l'appel de map.get si les modifications sont effectuées dans l'appel de la méthode computeIfPresent?.

Veuillez noter que je suis conscient que les modifications apportées à la liste ne seront pas visibles si je prenais référence à la liste avant de procéder à la mise à jour. Je ne sais pas si les modifications apportées à la liste seront visibles si je prends référence à la liste (en appelant map.get) après l'opération de mise à jour.

Je ne suis pas sûr de savoir comment interpréter la documentation, mais il me semble qu'une relation préalable garantira la visibilité des modifications apportées à la collection sous-jacente dans ce cas particulier.

De manière plus formelle, une opération de mise à jour pour une clé donnée porte une relation «passe-avant» avec toute extraction (non nulle) pour cette clé rapportant la valeur mise à jour

14
Marin Veršić

Pour clarifier votre question:

Vous fournissez une garantie externe telle que Map.computeIfPresent() est appelé avant Map.get().

Vous n'avez pas indiqué comment vous le faisiez, mais supposons que vous le fassiez en utilisant quelque chose avec la sémantique happen-before fournie par la JVM. Si tel est le cas, cela seul garantit que List.add() est visible pour le thread appelant Map.get() simplement parassociationde la relation happen-before.

Maintenant, pour répondre à la question que vous demandez en réalité: Comme vous l'avez indiqué, il existe une relation happen-before entre l'opération de mise à jour ConcurrentHashMap.computeIfPresent() et l'appel suivant de la méthode d'accès ConcurrentMap.get(). Et naturellement, il existe une relation arrive-avant- entre List.add() et la fin de ConcurrentHashMap.computeIfPresent().

Ensemble, la réponse est oui .

Il y a une garantie que l'autre thread verra 5 dans la List obtenue par Map.get(), à condition que vous garantissiez que Map.get() soit réellement appelé après computeIfPresent() se termine (comme indiqué dans la question). Si cette dernière garantie devient défectueuse et que Map.get() est appelé avant la fin de computeIfPresent(), il n'y a aucune garantie quant à ce que l'autre thread verra puisque ArrayList n'est pas thread-safe.

1
antak

Le fait que cette méthode soit documentée comme étant atomic ne signifie pas grand chose à propos de visibility (sauf si cela fait partie de la documentation). Par exemple, pour simplifier les choses:

// some shared data
private List<Integer> list = new ArrayList<>();

public synchronized void addToList(List<Integer> x){
     list.addAll(x);
}

public /* no synchronized */ List<Integer> getList(){
     return list;
}

On peut dire que addToList est en effet atomique, un seul thread à la fois peut l'appeler. Mais une fois qu'un certain fil appelle getList - il n'y a tout simplement aucune garantie sur visibility (car pour que cela soit établi, cela doit se produire sur le même verrou ). Donc, la visibilité est quelque chose qui se passe avant est concerné et la documentation computeIfPresent ne dit rien à ce sujet. 

Au lieu de cela, la documentation de la classe indique:

Les opérations de récupération (y compris get) ne bloquent généralement pas, donc chevauchement avec les opérations de mise à jour (y compris les opérations de vente et de suppression). 

Le point clé ici est évidemment chevauchement, de sorte que d'autres threads appelant get (obtenant ainsi une prise de contrôle de cette List), peuvent voir que List dans un état donné; pas nécessairement un état où computeIfPresent a commencé (avant que vous ayez appelé get). Assurez-vous de lire plus loin pour comprendre ce que cela signifie en fait certains

Et maintenant, la partie la plus délicate de cette documentation:

Les récupérations reflètent les résultats des dernières opérations de mise à jour achevées en cours. Plus formellement, une opération de mise à jour pour une clé donnée porte une relation happen-before avec toute extraction (non nulle) pour cette clé rapportant la valeur mise à jour.

Lisez à nouveau cette phrase à propos de terminé. Elle indique que la seule chose que vous pouvez lire lorsqu'un thread fait get est l'état de la dernière fin dans lequel se trouvait List. Et maintenant, la phrase suivante indique qu'il y a a se produit avant que ne soit établi entre deux actions. 

Pensez-y, un happens-before est établi entre deux actions ultérieures (comme dans l'exemple synchronisé ci-dessus); donc, en interne, lorsque vous mettez à jour une Key, il pourrait y avoir un signal écrit volatil indiquant que la mise à jour est terminée (je suis à peu près sûr que cela n’a pas été fait ainsi, juste un exemple). Pour que les événements précédents fonctionnent réellement, get doit lire cet élément volatile et voir l'état qui lui a été écrit; s'il voit cet état, cela signifie que se produit avant que ne soit établi; et je suppose que par une autre technique, cela est effectivement appliqué. 

Donc, pour répondre à votre question, tous les threads appelant get verront le last completed action qui s’est passé sur cette clé; dans votre cas, si vous pouvez garantir cette commande, je dirais, oui, elles seront visibles. 

4
Eugene

c.add(5) n'est pas thread-safe, l'état interne de c n'est pas protégé par la carte.

La méthode exacte pour créer des valeurs individuelles et des combinaisons insertion-utilisation-suppression thread-safe et exempt de conditions de concurrence dépend du modèle d'utilisation (encapsuleur synchronisé, copie sur écriture, file d'attente sans verrouillage, etc.).

2
bobah