web-dev-qa-db-fra.com

ConcurrentHashMap est-il totalement sûr?

c'est un passage de JavaDoc concernant ConcurrentHashMap. Il indique que les opérations de récupération ne sont généralement pas bloquées et peuvent donc chevaucher les opérations de mise à jour. Est-ce à dire que la méthode get() n'est pas thread-safe?

"Cependant, même si toutes les opérations sont thread-safe, les opérations de récupération n'entraînent pas de verrouillage, et il n'y a aucune prise en charge pour verrouiller la table entière d'une manière qui empêche tout accès. Cette classe est entièrement interopérable avec Hashtable dans les programmes qui s'appuient sur sa sécurité de thread mais pas sur ses détails de synchronisation.

Les opérations de récupération (y compris get) ne sont généralement pas bloquées, elles peuvent donc chevaucher les opérations de mise à jour (y compris put et remove). Les récupérations reflètent les résultats des opérations de mise à jour les plus récemment terminées et se poursuivent. "

46
user697911

La méthode get() est thread-safe, et les autres utilisateurs vous ont donné des réponses utiles concernant ce problème particulier.

Cependant, bien que ConcurrentHashMap soit un remplacement de thread-safe drop-in pour HashMap, il est important de réaliser que si vous effectuez plusieurs opérations, vous devrez peut-être modifier considérablement votre code. Par exemple, prenez ce code:

if (!map.containsKey(key)) 
   return map.put(key, value);
else
   return map.get(key);

Dans un environnement multi-thread, il s'agit d'une condition de concurrence. Vous devez utiliser ConcurrentHashMap.putIfAbsent(K key, V value) et faire attention à la valeur de retour, qui vous indique si l'opération put a réussi ou non. Lisez les documents pour plus de détails.


Répondre à un commentaire qui demande des éclaircissements sur la raison pour laquelle il s'agit d'une condition de concurrence.

Imaginez qu'il y a deux threads A, B qui vont mettre deux valeurs différentes dans la carte, v1 Et v2 Respectivement, ayant la même clé. La clé n'est initialement pas présente sur la carte. Ils s'entrelacent de cette façon:

  • Le thread A appelle containsKey et découvre que la clé n'est pas présente, mais est immédiatement suspendue.
  • Le thread B appelle containsKey et découvre que la clé n'est pas présente et a le temps d'insérer sa valeur v2.
  • Thread A reprend et insère v1, Écrasant "paisiblement" (puisque put est threadsafe) la valeur insérée par thread B.

Maintenant, le thread B "pense" qu'il a réussi à insérer sa propre valeur v2, Mais la carte contient v1. C'est vraiment un désastre car le thread B peut appeler v2.updateSomething() et "pensera" que les consommateurs de la carte (par exemple d'autres threads) ont accès à cet objet et verront cette mise à jour peut-être importante ("comme: cette adresse IP visiteur essaie de faire un DOS, refusez désormais toutes les requêtes"). Au lieu de cela, l'objet sera bientôt récupéré et perdu.

50
gd1

Il est thread-safe. Cependant, la façon dont il est thread-safe peut ne pas être celle que vous attendez. Il y a quelques "indices" que vous pouvez voir à partir de:

Cette classe est entièrement interopérable avec Hashtable dans les programmes qui dépendent de sa sécurité des threads mais pas de ses détails de synchronisation

Pour connaître toute l'histoire dans une image plus complète, vous devez connaître l'interface ConcurrentMap.

L'original Map fournit des méthodes de lecture/mise à jour très basiques. Même moi, j'ai pu faire une implémentation thread-safe de Map; il y a beaucoup de cas où les gens ne peuvent pas utiliser ma carte sans considérer mon mécanisme de synchronisation. Voici un exemple typique:

if (!threadSafeMap.containsKey(key)) {
   threadSafeMap.put(key, value);
}

Ce morceau de code n'est pas thread-safe, même si la carte elle-même l'est. Deux threads appelant containsKey() en même temps pourraient penser qu'il n'y a pas une telle clé qu'ils insèrent donc tous les deux dans Map.

Afin de résoudre le problème, nous devons explicitement effectuer une synchronisation supplémentaire. Supposons que la sécurité des threads de ma carte soit obtenue par des mots clés synchronisés, vous devrez faire:

synchronized(threadSafeMap) {
    if (!threadSafeMap.containsKey(key)) {
       threadSafeMap.put(key, value);
    }
}

Ce code supplémentaire nécessite que vous connaissiez les "détails de synchronisation" de la carte. Dans l'exemple ci-dessus, nous devons savoir que la synchronisation est réalisée par "synchronisé".

L'interface ConcurrentMap va plus loin. Il définit certaines actions "complexes" courantes qui impliquent un accès multiple à la carte. Par exemple, l'exemple ci-dessus est exposé sous la forme putIfAbsent(). Avec ces actions "complexes", les utilisateurs de ConcurrentMap (dans la plupart des cas) n'ont pas besoin de synchroniser les actions avec un accès multiple à la carte. Par conséquent, l'implémentation de Map peut effectuer un mécanisme de synchronisation plus compliqué pour de meilleures performances. ConcurrentHashhMap est un bon exemple. La sécurité des threads est en fait maintenue en conservant des verrous séparés pour les différentes partitions de la carte. Il est thread-safe car l'accès simultané à la carte ne corrompra pas la structure de données interne, ou entraînera une perte de mise à jour inattendue, etc.

Avec tout ce qui précède à l'esprit, la signification de Javadoc sera plus claire:

"Les opérations de récupération (y compris get) ne bloquent généralement pas" car ConcurrentHashMap n'utilise pas "synchronized" pour sa sécurité des threads. La logique de get elle-même s'occupe de la sécurité des threads; et si vous regardez plus loin dans le Javadoc:

La table est partitionnée en interne pour essayer d'autoriser le nombre indiqué de mises à jour simultanées sans contention

Non seulement la récupération n'est pas bloquante, mais même les mises à jour peuvent se produire simultanément. Cependant, non-blocage/mises à jour simultanées ne signifie pas qu'il est thread-UNsafe. Cela signifie simplement qu'il utilise d'autres moyens que le simple "synchronisé" pour la sécurité des threads.

Cependant, comme le mécanisme de synchronisation interne n'est pas exposé, si vous souhaitez effectuer des actions compliquées autres que celles fournies par ConcurrentMap, vous devrez peut-être envisager de modifier votre logique ou de ne pas utiliser ConcurrentHashMap . Par exemple:

// only remove if both key1 and key2 exists
if (map.containsKey(key1) && map.containsKey(key2)) {
    map.remove(key1);
    map.remove(key2);
}
17
Adrian Shum

ConcurrentHashmap.get() est thread-safe, dans le sens où

  • Il ne lèvera aucune exception, y compris ConcurrentModificationException
  • Il retournera un résultat qui était vrai à un moment (récent) dans le passé. Cela signifie que deux appels consécutifs à obtenir peuvent renvoyer des résultats différents. Bien sûr, cela vaut également pour tout autre Map.
10
Miserable Variable

HashMap est divisé en "seaux" basé sur hashCode. ConcurrentHashMap utilise ce fait. Son mécanisme de synchronisation est basé sur le blocage des compartiments plutôt que sur l'ensemble Map. De cette façon, peu de threads peuvent écrire simultanément dans plusieurs compartiments différents (un thread peut écrire dans un compartiment à la fois).

La lecture de ConcurrentHashMap presque n'utilise pas la synchronisation. La synchronisation est utilisée lorsque, lors de la récupération de la valeur de la clé, elle voit la valeur de null. Étant donné que ConcurrentHashMap ne peut pas stocker null en tant que valeurs (oui, à part les clés, les valeurs ne peuvent pas non plus être null s), cela suggère que l'extraction de null lors de la lecture s'est produite au milieu de l'initialisation de la carte entrée (paire clé-valeur) par un autre thread: quand la clé a été affectée, mais pas encore, et détient toujours par défaut null.
Dans ce cas, le fil de lecture devra attendre que l'écriture soit entièrement écrite.

Ainsi, les résultats de read() seront basés sur l'état actuel de la carte. Si vous lisez la valeur de la clé qui était en cours de mise à jour, vous obtiendrez probablement l'ancienne valeur car le processus d'écriture n'est pas encore terminé.

7
Pshemo

get () dans ConcurrentHashMap est thread-safe car il lit la valeur qui est volatile. Et dans les cas où la valeur est nulle d'une clé, la méthode get () attend jusqu'à ce qu'elle obtienne le verrou, puis lit la valeur mise à jour.

Lorsque la méthode put() met à jour CHM, elle définit la valeur de cette clé sur null, puis elle crée une nouvelle entrée et met à jour le CHM. Cette valeur nulle est utilisée par la méthode get() comme signal qu'un autre thread met à jour le CHM avec la même clé.

5
AKS

Cela signifie simplement que lorsqu'un thread est en cours de mise à jour et qu'un thread lit, il n'y a aucune garantie que celui qui a appelé la méthode ConcurrentHashMap en premier, à temps, verra son opération se produire en premier.

Pensez à une mise à jour sur l'élément indiquant où se trouve Bob. Si un thread demande où Bob se trouve à peu près en même temps qu'un autre thread se met à jour pour dire qu'il est venu `` à l'intérieur '', vous ne pouvez pas prédire si le thread du lecteur obtiendra le statut de Bob comme `` à l'intérieur '' ou `` à l'extérieur ''. Même si le thread de mise à jour appelle d'abord la méthode, le thread de lecture peut obtenir le statut "extérieur".

Les threads ne se poseront pas de problèmes. Le code est ThreadSafe.

Un thread n'entrera pas dans une boucle infinie ou ne commencera pas à générer des NullPointerExceptions bizarres ou à obtenir "itside" avec la moitié de l'ancien statut et la moitié du nouveau.

4
Lee Meador