Quelques réponses sur SO indiquent que la méthode get dans un HashMap peut tomber dans une boucle infinie (par exemple, celle-ci ou celle-ci ) si elle n'est pas synchronisée correctement (et généralement la partie inférieure). La ligne est "n'utilisez pas de HashMap dans un environnement multi-thread, utilisez un ConcurrentHashMap").
Bien que je puisse facilement voir pourquoi des appels simultanés à la méthode HashMap.put (Object) peuvent provoquer une boucle infinie, je ne comprends pas très bien pourquoi la méthode get (Object) peut rester bloquée lorsqu’elle tente de lire un HashMap en cours de redimensionnement à ce moment précis. J'ai jeté un œil à l'implémentation de openjdk et elle contient un cycle, mais la condition de sortie e != null
devrait être remplie tôt ou tard. Comment peut-il boucler pour toujours? Un élément de code mentionné explicitement comme vulnérable à ce problème est le suivant:
public class MyCache {
private Map<String,Object> map = new HashMap<String,Object>();
public synchronized void put(String key, Object value){
map.put(key,value);
}
public Object get(String key){
// can cause in an infinite loop in some JDKs!!
return map.get(key);
}
}
Quelqu'un peut-il expliquer comment un thread qui insère un objet dans HashMap et en lit une autre lecture peut s'entrelacer de manière à générer une boucle infinie? S'agit-il d'un problème de cohérence de la mémoire cache ou d'un réarrangement des instructions de l'UC (le problème ne peut donc se produire que sur une machine multiprocesseur)?
Votre lien concerne HashMap en Java 6. Il a été réécrit en Java 8. Avant cette réécriture, une boucle infinie sur get(Object)
était possible s'il y avait deux threads en écriture. Je ne suis pas au courant d'une manière dont la boucle infinie sur get
peut se produire avec un seul écrivain.
Plus précisément, la boucle infinie se produit quand il y a deux appels simultanés à resize(int)
qui appelle transfer
:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
Cette logique inverse le classement des noeuds dans le compartiment de hachage. Deux inversions simultanées peuvent faire une boucle.
Regarder:
e.next = newTable[i];
newTable[i] = e;
Si deux threads traitent le même nœud e
, le premier thread s'exécute normalement, mais le second thread définit e.next = e
, car newTable[i]
a déjà été défini sur e
par le premier thread. Le noeud e
pointe maintenant sur lui-même et, lorsque get(Object)
est appelé, il entre dans une boucle infinie.
En Java 8, le redimensionnement conserve l'ordre du noeud afin qu'une boucle ne puisse pas se produire de cette manière. Vous pouvez cependant perdre des données.
Les itérateurs de la classe LinkedHashMap
peuvent rester bloqués dans une boucle infinie lorsqu'il y a plusieurs lecteurs et aucun rédacteur lorsque la commande d'accès est conservée. Avec plusieurs lecteurs et un ordre d'accès, chaque lecture supprime puis insère le nœud auquel on a accédé à partir d'une liste de nœuds double liée. Plusieurs lecteurs peuvent amener le même nœud à être réinséré plusieurs fois dans la liste, provoquant une boucle. Encore une fois, le cours a été réécrit pour Java 8 et je ne sais pas si ce problème existe toujours ou non.
Situation:
La capacité par défaut de HashMap est 16 et le facteur de charge est de 0,75, ce qui signifie que HashMap doublera sa capacité lorsque la 12ème paire clé-valeur entre dans la carte (16 * 0,75 = 12).
Lorsque 2 threads tentent d'accéder simultanément à HashMap, vous risquez de rencontrer une boucle infinie. Les threads 1 et 2 tentent de mettre la 12ème paire clé-valeur.
Le fil 1 a une chance d'exécution:
Thread 1 après avoir pointé sur les paires clé-valeur et avant de commencer le processus de transfert, perdez le contrôle et Thread 2 a eu une chance d'être exécuté.
Le fil 2 a une chance d'exécution:
Le fil 1 a une chance d'exécution:
Solution:
Pour résoudre ce problème, utilisez un Collections.synchronizedMap
ou ConcurrentHashMap
.
ConcurrentHashMap est un thread-safe, c'est-à-dire que le code est accessible par un seul thread à la fois.
HashMap peut être synchronisé à l'aide de la méthode Collections.synchronizedMap(hashMap)
. En utilisant cette méthode, nous obtenons un objet HashMap équivalent à l’objet HashTable. Ainsi, chaque modification est effectuée sur la carte est verrouillé sur l'objet de la carte.
Étant donné que la seule possibilité que je vois pour une boucle infinie serait e.next = e
dans la méthode get
:
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next)
Et cela ne pouvait se produire que dans la méthode transfer
lors d'un redimensionnement:
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //here e.next could point on e if the table is modified by another thread
newTable[i] = e;
e = next;
} while (e != null);
Si un seul thread modifie la carte, je pense qu'il est tout à fait impossible d'avoir une boucle infinie avec un seul thread. C’était plus évident avec l’ancienne implémentation de get
avant le jdk 6 (ou 5):
public Object get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
Entry e = table[i];
while (true) {
if (e == null)
return e;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}
Même dans ce cas, le cas semble toujours assez improbable, sauf s’il ya beaucoup de collisions.
P.S: J'aimerais bien avoir tort!