La réinsertion d’un object dans ConcurrentHashMap provoque-t-elle une relation de mémoire «se produit-avant»?

Je travaille avec du code existant qui a un magasin d’objects sous la forme d’un ConcurrentHashMap. Dans la carte sont stockés des objects mutables, utilisés par plusieurs threads. Il n’y a pas deux threads qui tentent de modifier un object en même temps par conception. Ma préoccupation concerne la visibilité des modifications entre les threads.

Actuellement, le code de l’object est synchronisé sur les “setters” (gardés par l’object même). Il n’y a pas de synchronisation sur les “getters” ni les membres volatiles. Cela signifierait pour moi que la visibilité n’est pas garantie. Cependant, lorsqu’un object est modifié, il est réinséré dans la carte (la méthode put() est appelée à nouveau, même clé). Cela signifie-t-il que lorsqu’un autre thread extrait l’object de la carte, il verra les modifications?

J’ai effectué des recherches à ce sujet ici sur stackoverflow, dans JCIP et dans la description du paquetage pour java.util.concurrent. En gros, je me suis confondu je pense … mais la goutte qui m’a poussé à poser cette question était tirée de la description du colis, elle dit:

Les actions dans un thread avant de placer un object dans une collection simultanée ont lieu avant des actions consécutives à l’access ou à la suppression de cet élément de la collection dans un autre thread.

En relation avec ma question, les “actions” incluent-elles les modifications apscopes aux objects stockés dans la carte avant la re-put ()? Si tout cela crée de la visibilité sur les threads, s’agit-il d’une approche efficace? Je suis relativement nouveau dans les discussions et apprécierais vos commentaires.

Modifier:

Merci à tous pour vos réponses! C’était ma première question sur StackOverflow et cela m’a été très utile.

Je dois y aller avec la réponse de ptomli parce que je pense que cela a très clairement résolu mon problème. En l’occurrence, l’établissement d’une relation “se passe-avant” n’affecte pas nécessairement la visibilité de la modification dans ce cas. Ma “question de titre” est mal construite en ce qui concerne ma question actuelle décrite dans le texte. La réponse de ptomli correspond maintenant à ce que j’ai lu dans JCIP : “Pour que tous les threads voient les valeurs les plus récentes des variables mutables partagées, les threads en lecture et en écriture doivent se synchroniser sur un verrou commun” (page 37). Ré-insérer l’object dans la carte ne fournit pas ce verrou commun pour la modification des membres de l’object inséré.

J’apprécie tous les conseils de changement (objects immuables, etc.), et je suis entièrement d’accord. Mais dans ce cas, comme je l’ai mentionné, il n’ya pas de modification simultanée en raison d’une gestion minutieuse des threads. Un thread modifie un object et un autre lit ultérieurement l’object (le CHM étant le convoyeur d’object). Je pense que le CHM est insuffisant pour garantir que le thread d’exécution ultérieur verra les modifications depuis le premier compte tenu de la situation que j’ai fournie. Cependant, je pense que beaucoup d’entre vous ont correctement répondu à la question du titre .

Je pense que votre question concerne davantage les objects que vous stockez dans la carte et leur réaction à un access simultané que la carte simultanée elle-même.

Si les instances que vous stockez dans la carte ont des mutateurs synchronisés, mais pas des accesseurs synchronisés, je ne vois pas comment elles peuvent être sécurisées comme décrit.

Retirez la Map de l’équation et déterminez si les instances que vous stockez sont thread-safe par elles-mêmes.

Cependant, lorsqu’un object est modifié, il est réinséré dans la carte (la méthode put () est appelée à nouveau, même clé). Cela signifie-t-il que lorsqu’un autre thread extrait l’object de la carte, il verra les modifications?

Cela illustre la confusion. L’instance qui est réinsérée dans la carte sera extraite de la carte par un autre thread. C’est la garantie de la carte concurrente. Cela n’a rien à voir avec la visibilité de l’état de l’instance stockée elle-même.

Vous appelez concurrHashMap.put après chaque écriture dans un object. Cependant, vous n’avez pas précisé que vous appelez également concurrHashMap.get avant chaque lecture. Ceci est nécessaire.

Ceci est vrai pour toutes les formes de synchronisation: vous devez avoir des “points de contrôle” dans les deux threads. La synchronisation d’un seul thread est inutile.

Je n’ai pas vérifié le code source de ConcurrentHashMap pour m’assurer que les options put et get déclenchent un événement précédent, mais il est logique qu’elles le soient.

Cependant, votre méthode pose toujours problème, même si vous utilisez à la fois put et get . Le problème survient lorsque vous modifiez un object et qu’il est utilisé (dans un état incohérent) par l’autre thread avant sa put . C’est un problème subtil parce que vous pourriez penser que l’ancienne valeur serait lue puisqu’elle n’a pas encore été put et qu’elle ne poserait pas de problème. Le problème est que lorsque vous ne synchronisez pas, vous n’êtes pas assuré d’obtenir un object ancien cohérent, mais le comportement n’est pas défini. La machine virtuelle Java peut mettre à jour à tout moment n’importe quelle partie de l’object dans les autres threads. Ce n’est que lorsque vous utilisez une synchronisation explicite que vous êtes sûr de mettre à jour les valeurs de manière cohérente sur tous les threads.

Ce que tu pourrais faire:
(1) synchroniser tous les access (getters et setters) à vos objects partout dans le code. Soyez prudent avec les setters: assurez-vous que vous ne pouvez pas définir l’object dans un état incohérent. Par exemple, lorsque vous définissez le prénom et le nom, il ne suffit pas d’avoir deux parameters synchronisés: vous devez obtenir le locking d’object pour les deux opérations ensemble.
ou
(2) lorsque vous put un object dans la carte, placez une copie complète à la place de l’object lui-même. Ainsi, les autres threads ne liront jamais d’object dans un état incohérent.

EDIT :
je viens de remarquer

Actuellement, le code de l’object est synchronisé sur les “setters” (gardés par l’object même). Il n’y a pas de synchronisation sur les “getters” ni les membres volatiles.

Ce n’est pas bien. Comme je l’ai dit plus haut, la synchronisation sur un seul thread n’est pas du tout une synchronisation. Vous pouvez synchroniser sur tous vos threads d’écriture, mais peu importe, les lecteurs n’obtiendront pas les bonnes valeurs.

Je pense que cela a déjà été dit dans plusieurs réponses, mais pour résumer

Si votre code va

  • CHM # get
  • appeler différents setters
  • CHM # put

alors le “arrive-avant” fourni par le put garantira que tous les appels mutés seront exécutés avant le put. Cela signifie que tout achat ultérieur sera assuré de voir ces changements.

Votre problème est que l’état réel de l’object ne sera pas déterministe car si le stream réel des événements est

  • fil 1: CHM # get
  • fil 1: appeleur
  • fil 2: CHM # get
  • fil 1: appeleur
  • fil 1: appeleur
  • fil 1: CHM # put

alors il n’y a aucune garantie sur l’état de l’object dans le thread 2. Il peut voir l’object avec la valeur fournie par le premier installateur ou ne pas le voir.

La copie immuable serait la meilleure approche car seuls les objects parfaitement cohérents sont publiés. Rendre les différents parameters synchronisés (ou les références sous-jacentes volatiles) ne vous permet toujours pas de publier un état cohérent, cela signifie simplement que l’object verra toujours la dernière valeur pour chaque getter à chaque appel.

D’après ce que je comprends, cela devrait fonctionner pour tout le monde après la réinstallation, mais ce serait une méthode de synchronisation très dangereuse.

Qu’est-ce qui arrive à cela arrive avant la réinstallation, mais pendant que des modifications sont en cours. Ils peuvent ne voir que certains des changements, et l’object aurait un état incohérent.

Si vous le pouvez, je vous conseillerais de stocker des objects immuables dans la carte. Ensuite, tout get récupérera une version de l’object qui était courante au moment où il l’a obtenu.

C’est un extrait de code provenant de java.util.concurrent.ConcurrentHashMap (Open JDK 7):

  919 public V get(Object key) { 920 Segment s; // manually integrate access methods to reduce overhead 921 HashEntry[] tab; 922 int h = hash(key.hashCode()); 923 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; 924 if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && 925 (tab = s.table) != null) { 926 for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile 927 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); 928 e != null; e = e.next) { 929 K k; 930 if ((k = e.key) == key || (e.hash == h && key.equals(k))) 931 return e.value; 932 } 933 } 934 return null; 935 } 

UNSAFE.getObjectVolatile() est documenté comme getter avec une sémantique interne volatile , ainsi la barrière de mémoire sera franchie lors de l'obtention de la référence.

oui, put entraîne une écriture volatile, même si la valeur clé existe déjà dans la carte.

L’utilisation de ConcurrentHashMap pour publier des objects sur plusieurs threads est relativement efficace. Les objects ne doivent plus être modifiés une fois qu’ils sont sur la carte. (Ils ne doivent pas être ssortingctement immuables (avec les champs finaux))