Il existe un cas où une carte sera construite et une fois qu'elle sera initialisée, elle ne sera plus jamais modifiée. Cependant, il sera accessible (via get (clé) uniquement) à partir de plusieurs threads. Est-il prudent d'utiliser un Java.util.HashMap
de cette manière?
(Actuellement, j’utilise heureusement un Java.util.concurrent.ConcurrentHashMap
, et n’ai aucun besoin réel d’améliorer les performances, mais je suis simplement curieux de savoir si un simple HashMap
suffirait. Par conséquent, cette question est non "Lequel dois-je utiliser?" s’agit-il d’une question de performance, mais plutôt de la question "serait-il sans danger?")
Jeremy Manson, le dieu en ce qui concerne le modèle de mémoire Java, a un blog en trois parties sur ce sujet - parce que vous posez essentiellement la question "Est-il sécuritaire d'accéder à un HashMap immuable"? Mais vous devez répondre au prédicat de cette question: "Mon HashMap est-il immuable". La réponse pourrait vous surprendre - Java dispose d'un ensemble de règles relativement compliqué pour déterminer l'immutabilité.
Pour plus d'informations sur le sujet, lisez les articles du 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
Les lectures sont sécurisées 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 par les développeurs Java, y compris ici sur Stackoverflow. (Observez la note de cette réponse pour preuve.)
Si d'autres threads sont en cours d'exécution, ils risquent de ne pas voir une copie mise à jour de HashMap s'il n'y a pas d'écriture mémoire dans le thread actuel. Les écritures en mémoire se produisent via l'utilisation de mots-clés synchronisés ou volatils, ou via l'utilisation de certaines constructions Java de simultanéité.
Voir L'article de Brian Goetz sur le nouveau modèle de mémoire Java pour plus de détails.
Après un peu plus de recherche, j'ai trouvé ceci dans le Java doc (souligné par moi):
Notez que cette implémentation n'est pas synchronisé. Si plusieurs threads accéder à une carte de hachage simultanément et à au moins un des threads modifie le fichier carte structurellement, il doit être synchronisé en externe. (Une modification structurelle Est une opération qui ajoute ou supprime un ou plusieurs mappages; Qui ne fait que changer la valeur associée À une clé déjà contenue Contient n'est pas structurel modification.)
Cela semble impliquer que ce sera sûr, en supposant que l'inverse de la déclaration est vrai.
Une remarque est que dans certaines circonstances, un get () d'un HashMap non synchronisé peut provoquer une boucle infinie. Cela peut se produire si un put () concurrent entraîne une modification de la carte.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
Il y a cependant une torsion importante. L'accès à la carte est sûr, 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) que HashMap. Cela peut se produire sur des systèmes multiprocesseurs où les modifications apportées à HashMap par un thread (par exemple, celui qui l'a rempli) peuvent rester dans le cache de ce CPU et ne seront pas vues par les threads s'exécutant sur d'autres CPU, jusqu'à ce qu'une opération de barrière de mémoire soit effectuée. effectué en veillant à la cohérence du cache. La spécification du langage Java est explicite sur celle-ci: la solution consiste à acquérir un verrou (synchronisé (...)) qui émet une opération de barrière de mémoire. Donc, si vous êtes certain qu'après avoir rempli le HashMap, chacun des threads acquiert N'IMPORTE QUEL verrou, vous pouvez à partir de là accéder au HashMap à partir de n'importe quel thread jusqu'à ce que HashMap soit à nouveau modifié.
Selon http://www.ibm.com/developerworks/Java/library/j-jtp03304/ # sécurité d'initialisation, vous pouvez transformer votre HashMap en champ définitif et le publier en toute sécurité.
... Dans le nouveau modèle de mémoire, il existe quelque chose de similaire à une relation "passe-avant" entre l'écriture d'un dernier champ dans un constructeur et le chargement initial d'une référence partagée à cet objet dans un autre thread . ...
Le scénario que vous avez décrit est donc que vous devez placer une série de données dans une carte, puis lorsque vous avez fini de la remplir, vous la considérez comme étant immuable. Une approche "sûre" (ce qui signifie que vous indiquez que le traitement est considéré comme immuable) consiste à remplacer la référence par Collections.unmodifiableMap(originalMap)
lorsque vous êtes prêt à le rendre immuable.
Pour un exemple d'erreur d'échec de mappage de mappage si vous l'utilisez simultanément et pour la solution de contournement que j'ai mentionnée, consultez l'entrée de cette parade de bogues: bug_id = 6423457
Soyez averti que même dans le code à thread unique, le remplacement d'un ConcurrentHashMap par un HashMap peut ne pas être sûr. ConcurrentHashMap interdit null en tant que clé ou valeur. HashMap ne leur interdit pas (ne demandez pas).
Ainsi, dans le cas peu probable où votre code existant pourrait ajouter une valeur null à la collection lors de l’installation (vraisemblablement en cas d’échec), le remplacement de la collection comme décrit modifiera le comportement fonctionnel.
Cela dit, à condition de ne rien faire d'autre, les lectures concurrentes d'un HashMap sont sécurisées.
[Edit: par "lectures concurrentes", je veux dire qu'il n'y a pas aussi de modifications concurrentes.
D'autres réponses expliquent comment y parvenir. Une solution consiste à 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 au thread A avant le démarrage du 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 a pour but de signaler que même en dehors des problèmes de simultanéité, il existe au moins une différence d'API entre ConcurrentHashMap et HashMap, ce qui pourrait perturber même un programme à thread unique qui remplace l'une par l'autre.]
Si l'initialisation et chaque vente sont synchronisées, vous êtes sauvegardé.
Le code suivant est sauvegardé car le chargeur de classe se chargera de la synchronisation:
public static final HashMap<String, String> map = new HashMap<>();
static {
map.put("A","A");
}
Le code suivant est sauvegardé car l’écriture de volatile prendra en charge la synchronisation.
class Foo {
volatile HashMap<String, String> map;
public void init() {
final HashMap<String, String> tmp = new HashMap<>();
tmp.put("A","A");
// writing to volatile has to be after the modification of the map
this.map = tmp;
}
}
Cela fonctionnera également si la variable membre est final, car final est également volatile. Et si la méthode est un constructeur.
http://www.docjar.com/html/api/Java/util/HashMap.Java.html
voici la source de HashMap. Comme vous pouvez le constater, il n’ya absolument aucun code de verrouillage/mutex.
Cela signifie que s'il est correct de lire 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é.