Il y a un cas où une carte sera construite, et une fois qu'elle est initialisée, elle ne sera plus jamais modifiée. Il sera cependant accessible (via get (key) uniquement) à partir de plusieurs threads. Est-il sûr d'utiliser un java.util.HashMap
de cette manière?
(Actuellement, j'utilise avec plaisir un java.util.concurrent.ConcurrentHashMap
, et je n'ai aucun besoin mesuré pour améliorer les performances, mais je suis simplement curieux de savoir si un simple HashMap
suffirait. Par conséquent, cette question n'est pas "Lequel dois-je utiliser?", Ni une question de performances. La question est plutôt "Serait-ce sûr?")
java
multithreading
concurrency
hashmap
Dave L.
la source
la source
ClassLoader
. Cela vaut une question distincte en soi. Je le synchroniserais toujours explicitement et le profil pour vérifier qu'il causait de réels problèmes de performances.Map
, ce que vous n'avez pas expliqué. Si vous ne le faites pas en toute sécurité, ce n'est pas sûr. Si vous le faites en toute sécurité, c'est le cas . Détails dans ma réponse.Réponses:
Votre idiome est en sécurité si et seulement si la référence à
HashMap
est publiée en toute sécurité . Plutôt que tout ce qui concerne les éléments internes d'HashMap
elle-même, la publication sécurisée traite de la façon dont le thread de construction rend la référence à la carte visible aux autres threads.Fondamentalement, la seule course possible ici se situe entre la construction du
HashMap
et les fils de lecture qui peuvent y accéder avant qu'il ne soit entièrement construit. La majeure partie de la discussion porte sur ce qui arrive à l'état de l'objet de la carte, mais cela n'a pas d'importance puisque vous ne le modifiez jamais - donc la seule partie intéressante est la façon dont laHashMap
référence est publiée.Par exemple, imaginez que vous publiez la carte comme ceci:
... et à un moment donné
setMap()
est appelé avec une carte, et d'autres threads utilisentSomeClass.MAP
pour accéder à la carte, et vérifiez la valeur null comme ceci:Ce n'est pas sûr même si cela semble probablement le cas. Le problème est qu'il n'y a pas se produit, avant la relation entre l'ensemble des
SomeObject.MAP
et la lecture ultérieure sur un autre fil, de sorte que le fil de lecture est libre de voir une carte partiellement construite. Cela peut faire n'importe quoi et même en pratique, cela fait des choses comme mettre le fil de lecture dans une boucle infinie .Pour publier la carte en toute sécurité, vous devez établir une relation d' avance entre l' écriture de la référence à la
HashMap
(c.-à-d. La publication ) et les lecteurs suivants de cette référence (c.-à-d. La consommation). Idéalement, il n'y a que quelques - uns faciles à retenir les moyens d' y parvenir que [1] :Les plus intéressants pour votre scénario sont (2), (3) et (4). En particulier, (3) s'applique directement au code que j'ai ci-dessus: si vous transformez la déclaration de
MAP
en:alors tout est casher: les lecteurs qui voient une non nulle valeur ont nécessairement se passe-avant relation avec le magasin pour
MAP
et donc voir tous les magasins associés à l'initialisation de la carte.Les autres méthodes changent la sémantique de votre méthode, puisque les deux (2) (en utilisant l'initaliseur statique) et (4) (en utilisant final ) impliquent que vous ne pouvez pas définir
MAP
dynamiquement au moment de l'exécution. Si vous n'avez pas besoin de le faire, déclarez simplement enMAP
tant questatic final HashMap<>
et vous êtes assuré d'une publication sécurisée.En pratique, les règles sont simples pour un accès sécurisé aux "objets jamais modifiés":
Si vous publiez un objet qui n'est pas intrinsèquement immuable (comme dans tous les champs déclarés
final
) et:final
champ (y comprisstatic final
pour les membres statiques).C'est tout!
En pratique, c'est très efficace. L'utilisation d'un
static final
champ, par exemple, permet à la JVM de supposer que la valeur est inchangée pendant la durée de vie du programme et de l'optimiser fortement. L'utilisation d'unfinal
champ membre permet à la plupart des architectures de lire le champ d'une manière équivalente à une lecture de champ normale et n'empêche pas d'autres optimisations c .Enfin, l'utilisation de
volatile
a un certain impact: aucune barrière matérielle n'est nécessaire sur de nombreuses architectures (telles que x86, en particulier celles qui ne permettent pas aux lectures de passer des lectures), mais une optimisation et une réorganisation peuvent ne pas se produire au moment de la compilation - mais cela l'effet est généralement faible. En échange, vous obtenez en fait plus que ce que vous avez demandé - non seulement vous pouvez en publier un en toute sécuritéHashMap
, mais vous pouvez stocker autant deHashMap
s non modifiés que vous le souhaitez sur la même référence et être assuré que tous les lecteurs verront une carte publiée en toute sécurité. .Pour plus de détails sanglants, reportez-vous à Shipilev ou à cette FAQ de Manson et Goetz .
[1] Citation directe de shipilev .
a Cela semble compliqué, mais ce que je veux dire, c'est que vous pouvez attribuer la référence au moment de la construction - soit au point de déclaration, soit dans le constructeur (champs membres) ou l'initialiseur statique (champs statiques).
b En option, vous pouvez utiliser une
synchronized
méthode pour obtenir / définir, ou unAtomicReference
ou quelque chose, mais nous parlons du travail minimum que vous pouvez faire.c Certaines architectures avec des modèles de mémoire très faibles (je regarde vous , Alpha) peuvent nécessiter un certain type de barrière de lecture avant une
final
lecture - mais ceux - ci sont très rares aujourd'hui.la source
never modify
HashMap
ne signifie pas que lestate of the map object
thread est sûr, je pense. Dieu connaît l'implémentation de la bibliothèque, si le document officiel ne dit pas qu'elle est thread-safe.get()
pourrait en fait effectuer des écritures, par exemple la mise à jour de certaines statistiques (ou dans le cas de laLinkedHashMap
mise à jour de l'ordre d'accès par ordre d'accès). Donc, une classe bien écrite devrait fournir une documentation qui indique clairement si ...const
sont vraiment en lecture seule dans ce sens (en interne, elles peuvent toujours effectuer des écritures, mais celles-ci devront être sécurisées pour les threads). Il n'y a pas deconst
mot-clé en Java et je ne connais aucune garantie globale documentée, mais en général, les classes de bibliothèque standard se comportent comme prévu et les exceptions sont documentées (voir l'LinkedHashMap
exemple où les opérations RO commeget
sont explicitement mentionnées comme dangereuses).HashMap
nous avons en fait dans la documentation le comportement de sécurité des threads pour cette classe: si plusieurs threads accèdent à une carte de hachage simultanément, et qu'au moins l'un des threads modifie la carte structurellement, il doit être synchronisé en externe. (Une modification structurelle est toute opération qui ajoute ou supprime un ou plusieurs mappages; le simple fait de changer la valeur associée à une clé qu'une instance contient déjà n'est pas une modification structurelle.)HashMap
méthodes que nous prévoyons d'être en lecture seule, elles sont en lecture seule, car elles ne modifient pas structurellement le fichierHashMap
. Bien sûr, cette garantie peut ne pas être valable pour d'autresMap
implémentations arbitraires , mais la question concerneHashMap
spécifiquement.Jeremy Manson, le dieu quand il s'agit du modèle de mémoire Java, a un blog en trois parties sur ce sujet - parce que vous vous posez essentiellement la question "Est-il sûr d'accéder à un HashMap immuable" - la réponse est oui. Mais vous devez répondre au prédicat à cette question qui est - "Mon HashMap est-il immuable". La réponse pourrait vous surprendre: Java a un ensemble de règles relativement compliqué pour déterminer l'immuabilité.
Pour plus d'informations sur le sujet, lisez les articles de blog de Jeremy:
Partie 1 sur l'immuabilité en Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html
Partie 2 sur l'immuabilité en Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html
Partie 3 sur l'immuabilité en Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html
la source
Les lectures sont sûres du point de vue de la synchronisation mais pas du point de vue de la mémoire. C'est quelque chose qui est largement mal compris parmi les développeurs Java, y compris ici sur Stackoverflow. (Observez la note de cette réponse pour preuve.)
Si vous avez d'autres threads en cours d'exécution, ils peuvent ne pas voir une copie mise à jour du HashMap s'il n'y a pas d'écriture de mémoire dans le thread actuel. Les écritures en mémoire se produisent via l'utilisation des mots clés synchronisés ou volatils, ou via l'utilisation de certaines constructions de concurrence Java.
Voir l'article de Brian Goetz sur le nouveau modèle de mémoire Java pour plus de détails.
la source
Après un peu plus de recherche, j'ai trouvé ceci dans la doc java (c'est moi qui souligne):
Cela semble impliquer que ce sera sûr, en supposant que l'inverse de l'énoncé est vrai.
la source
Une note est que dans certaines circonstances, un get () à partir d'un HashMap non synchronisé peut provoquer une boucle infinie. Cela peut se produire si un put () simultané provoque une refonte de la carte.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
la source
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
Il y a cependant une torsion importante. Il est sûr d'accéder à la carte, mais en général, il n'est pas garanti que tous les threads verront exactement le même état (et donc les mêmes valeurs) du HashMap. Cela peut se produire sur les systèmes multiprocesseurs où les modifications apportées au HashMap par un thread (par exemple, celui qui l'a rempli) peuvent se trouver dans le cache de ce processeur et ne seront pas vues par les threads exécutés sur d'autres processeurs, jusqu'à ce qu'une opération de clôture de mémoire soit effectuée en assurant la cohérence du cache. La spécification du langage Java est explicite sur celle-ci: la solution est d'acquérir un verrou (synchronisé (...)) qui émet une opération de clôture mémoire. Donc, si vous êtes sûr qu'après avoir rempli le HashMap, chacun des threads acquiert N'IMPORTE QUEL verrou, alors vous pouvez à partir de ce moment accéder au HashMap à partir de n'importe quel thread jusqu'à ce que le HashMap soit à nouveau modifié.
la source
Selon http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Sécurité d'initialisation, vous pouvez faire de votre HashMap un champ final et une fois que le constructeur aura terminé, il sera publié en toute sécurité.
... Sous le nouveau modèle de mémoire, il y a quelque chose de similaire à une relation d'avance entre l'écriture d'un champ final dans un constructeur et la charge initiale d'une référence partagée à cet objet dans un autre thread. ...
la source
Ainsi, le scénario que vous avez décrit est que vous devez mettre un tas de données dans une carte, puis lorsque vous avez terminé de la remplir, vous la traitez comme immuable. Une approche "sûre" (ce qui signifie que vous faites en sorte qu'elle soit réellement traitée comme immuable) consiste à remplacer la référence par
Collections.unmodifiableMap(originalMap)
lorsque vous êtes prêt à la rendre immuable.Pour un exemple de la façon dont les cartes peuvent échouer si elles sont utilisées simultanément, et la solution de contournement suggérée que j'ai mentionnée, consultez cette entrée de parade de bogues: bug_id = 6423457
la source
Cette question est abordée dans le livre "Java Concurrency in Practice" de Brian Goetz (extrait 16.8, page 350):
Étant donné que
states
est déclaré asfinal
et que son initialisation est effectuée dans le constructeur de classe du propriétaire, tout thread qui lira plus tard cette carte est assuré de la voir à la fin du constructeur, à condition qu'aucun autre thread n'essaie de modifier le contenu de la carte.la source
Soyez averti que même dans un code à thread unique, le remplacement d'un ConcurrentHashMap par un HashMap peut ne pas être sûr. ConcurrentHashMap interdit null comme clé ou valeur. HashMap ne les interdit pas (ne demandez pas).
Donc, dans la situation peu probable où votre code existant pourrait ajouter une valeur null à la collection lors de l'installation (probablement dans un cas d'échec quelconque), le remplacement de la collection comme décrit modifiera le comportement fonctionnel.
Cela dit, à condition que vous ne fassiez rien d'autre, les lectures simultanées à partir d'un HashMap sont sûres.
[Edit: par "lectures simultanées", je veux dire qu'il n'y a pas également de modifications simultanées.
D'autres réponses expliquent comment s'en assurer. Une façon est de rendre la carte immuable, mais ce n'est pas nécessaire. Par exemple, le modèle de mémoire JSR133 définit explicitement le démarrage d'un thread comme une action synchronisée, ce qui signifie que les modifications apportées dans le thread A avant de démarrer le thread B sont visibles dans le thread B.
Mon intention n'est pas de contredire ces réponses plus détaillées sur le modèle de mémoire Java. Cette réponse vise à souligner que même en dehors des problèmes de concurrence, il existe au moins une différence d'API entre ConcurrentHashMap et HashMap, ce qui pourrait même saboter un programme à un seul thread qui remplaçait l'un par l'autre.]
la source
http://www.docjar.com/html/api/java/util/HashMap.java.html
voici la source de HashMap. Comme vous pouvez le constater, il n'y a absolument aucun code de verrouillage / mutex.
Cela signifie que même s'il est correct de lire à partir d'un HashMap dans une situation multithread, j'utiliserais certainement un ConcurrentHashMap s'il y avait plusieurs écritures.
Ce qui est intéressant, c'est que .NET HashTable et Dictionary <K, V> ont un code de synchronisation intégré.
la source
Si l'initialisation et chaque put est synchronisé, vous êtes sauvegardé.
Le code suivant est enregistré car le classloader se chargera de la synchronisation:
Le code suivant est sauvegardé car l'écriture de volatile se chargera de la synchronisation.
Cela fonctionnera également si la variable membre est finale car final est également volatile. Et si la méthode est un constructeur.
la source