J'ai une carte qui doit être modifiée par plusieurs threads simultanément.
Il semble y avoir trois implémentations de cartes synchronisées différentes dans l'API Java:
Hashtable
Collections.synchronizedMap(Map)
ConcurrentHashMap
D'après ce que j'ai compris, Hashtable
est une ancienne implémentation (extension de la classe obsolète Dictionary
), qui a été adaptée ultérieurement pour s'adapter à l'interface Map
. Bien que soit synchronisé, il semble que le problème soit sérieux problèmes d’évolutivité et il est déconseillé pour les nouveaux projets.
Mais qu'en est-il des deux autres? Quelles sont les différences entre les cartes renvoyées par Collections.synchronizedMap(Map)
et ConcurrentHashMap
s? Lequel correspond à quelle situation?
Pour vos besoins, utilisez ConcurrentHashMap
. Il permet la modification simultanée de la carte à partir de plusieurs threads sans qu'il soit nécessaire de les bloquer. Collections.synchronizedMap(map)
crée une mappe de blocage qui dégradera les performances, tout en garantissant la cohérence (si utilisé correctement).
Utilisez la deuxième option si vous devez assurer la cohérence des données et si chaque thread doit avoir une vue à jour de la carte. Utilisez le premier si les performances sont critiques et que chaque thread insère uniquement des données dans la carte, les lectures étant moins fréquentes.
╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗
║ Property ║ HashMap ║ Hashtable ║ ConcurrentHashMap ║
╠═══════════════╬═══════════════════╬═══════════════════╩═════════════════════╣
║ Null ║ allowed ║ not allowed ║
║ values/keys ║ ║ ║
╠═══════════════╬═══════════════════╬═════════════════════════════════════════╣
║Is thread-safe ║ no ║ yes ║
╠═══════════════╬═══════════════════╬═══════════════════╦═════════════════════╣
║ Lock ║ not ║ locks the whole ║ locks the portion ║
║ mechanism ║ applicable ║ map ║ ║
╠═══════════════╬═══════════════════╩═══════════════════╬═════════════════════╣
║ Iterator ║ fail-fast ║ weakly consistent ║
╚═══════════════╩═══════════════════════════════════════╩═════════════════════╝
En ce qui concerne le mécanisme de verrouillage: Hashtable
verrouille l'objet , alors que ConcurrentHashMap
verrouille uniquement le compartiment .
Les "problèmes d’évolutivité" de Hashtable
se présentent exactement de la même manière dans Collections.synchronizedMap(Map)
- ils utilisent une synchronisation très simple, ce qui signifie qu’un seul thread peut accéder à la carte en même temps.
Ce n'est pas vraiment un problème lorsque vous avez de simples insertions et des recherches (sauf si vous le faites de manière extrêmement intensive), mais cela devient un gros problème lorsque vous devez parcourir l'ensemble de la carte, ce qui peut prendre beaucoup de temps pour une grande un fil fait cela, tous les autres doivent attendre s'ils veulent insérer ou rechercher quoi que ce soit.
La ConcurrentHashMap
utilise des techniques très sophistiquées pour réduire le besoin de synchronisation et permettre un accès en lecture parallèle à plusieurs threads sans synchronisation. Elle fournit surtout une Iterator
ne nécessitant aucune synchronisation et permettant même la modification de si les éléments insérés lors de l'itération seront retournés).
ConcurrentHashMap est préférable lorsque vous pouvez l'utiliser - même si cela nécessite au moins Java 5.
Il est conçu pour évoluer correctement lorsqu'il est utilisé par plusieurs threads. Les performances peuvent être légèrement inférieures lorsque seulement un seul thread accède à la carte à la fois, mais nettement meilleures lorsque plusieurs threads accèdent à la carte simultanément.
J'ai trouvé un entrée de blog qui reproduit un tableau de l'excellent livre Java Concurrency In Practice , que je recommande vivement.
Collections.synchronizedMap n’a vraiment de sens que si vous avez besoin d’envelopper une carte avec d’autres caractéristiques, par exemple une carte ordonnée, comme une TreeMap.
La principale différence entre ces deux méthodes est que ConcurrentHashMap
ne verrouille qu'une partie des données en cours de mise à jour, tandis que d'autres parties peuvent accéder à d'autres données. Cependant, Collections.synchronizedMap()
verrouille toutes les données lors de la mise à jour, les autres threads ne peuvent accéder aux données que lorsque le verrou est libéré. S'il existe de nombreuses opérations de mise à jour et un nombre relativement faible d'opérations de lecture, vous devez choisir ConcurrentHashMap
.
De plus, une autre différence est que ConcurrentHashMap
ne conservera pas l'ordre des éléments dans la carte transmise. Il est similaire à HashMap
lors du stockage de données. Il n'y a aucune garantie que l'ordre des éléments est préservé. Alors que Collections.synchronizedMap()
préservera l'ordre des éléments de la carte transmise. Par exemple, si vous passez TreeMap
à ConcurrentHashMap
, l'ordre des éléments dans ConcurrentHashMap
ne sera peut-être pas le même que celui de TreeMap
, mais Collections.synchronizedMap()
préservera cet ordre.
De plus, ConcurrentHashMap
peut garantir qu'il n'y a pas de ConcurrentModificationException
levée lorsqu'un thread met à jour la carte et qu'un autre thread traverse l'itérateur obtenu à partir de la carte. Cependant, Collections.synchronizedMap()
n'est pas garanti à ce sujet.
Il y a un poste qui montre les différences entre eux et aussi la ConcurrentSkipListMap
.
Dans ConcurrentHashMap
, le verrou est appliqué à un segment au lieu d'une carte entière… .. Chaque segment gère sa propre table de hachage interne Le verrou est appliqué uniquement pour les opérations de mise à jour. Collections.synchronizedMap(Map)
synchronise la totalité de la carte.
Hashtable
et ConcurrentHashMap
n'autorise pas les clés null
ni les valeurs null
.
Collections.synchronizedMap(Map)
synchronise toutes les opérations (get
, put
, size
, etc.).
ConcurrentHashMap
prend en charge la simultanéité complète des extractions et la simultanéité ajustable des mises à jour.
Comme d'habitude, il existe des compromis entre la concurrence, les frais généraux et la vitesse. Vous devez vraiment prendre en compte les exigences détaillées en matière de simultanéité de votre application pour prendre une décision, puis tester votre code pour voir s'il est assez bon.
Vous avez raison à propos de HashTable
, vous pouvez l’oublier.
Votre article mentionne le fait que, bien que HashTable et la classe d'encapsulation synchronisée fournissent une sécurité de thread de base en permettant uniquement à un thread d'accéder à la carte, il ne s'agit pas d'une sécurité de thread "vraie", car de nombreuses opérations composées nécessitent encore synchronisation supplémentaire, par exemple:
synchronized (records) {
Record rec = records.get(id);
if (rec == null) {
rec = new Record(id);
records.put(id, rec);
}
return rec;
}
Cependant, ne pensez pas que ConcurrentHashMap
est une alternative simple à un HashMap
avec un bloc synchronized
typique, comme indiqué ci-dessus. Lisez ceci article pour mieux comprendre ses subtilités.
Carte synchronisée:
Synchronized Map n’est également pas très différent de Hashtable et offre des performances similaires dans les programmes Java simultanés. La seule différence entre Hashtable et SynchronizedMap est que SynchronizedMap n’est pas un héritage et vous pouvez envelopper toute Map pour en créer la version synchronisée à l’aide de la méthode Collections.synchronizedMap ().
ConcurrentHashMap:
La classe ConcurrentHashMap fournit une version simultanée de HashMap standard. Il s'agit d'une amélioration de la fonctionnalité synchronizedMap fournie dans la classe Collections.
Contrairement à Hashtable et Synchronized Map, il ne verrouille jamais l'ensemble de la carte. Il divise la carte en segments et le verrouillage est effectué sur ceux-ci. Cela fonctionne mieux si le nombre de threads de lecteur est supérieur au nombre de threads d'écriture.
ConcurrentHashMap est par défaut séparé en 16 régions et des verrous sont appliqués. Ce numéro par défaut peut être défini lors de l'initialisation d'une instance de ConcurrentHashMap. Lors du paramétrage de données dans un segment particulier, le verrou pour ce segment est obtenu. Cela signifie que deux mises à jour peuvent toujours être exécutées simultanément en toute sécurité si elles affectent chacune des compartiments distincts, minimisant ainsi les conflits de verrous et optimisant les performances.
ConcurrentHashMap ne lève pas une ConcurrentModificationException
ConcurrentHashMap ne lève pas d'exception ConcurrentModificationException si un thread tente de le modifier pendant qu'un autre itère dessus
Différence entre synchornizedMap et ConcurrentHashMap
Collections.synchornizedMap (HashMap) retournera une collection presque équivalente à Hashtable, où chaque opération de modification sur la carte est verrouillée sur un objet Map, tandis que dans le cas de ConcurrentHashMap, la sécurité des threads est obtenue en divisant la carte entière en une partition différente en fonction du niveau de concurrence et verrouiller uniquement une partie particulière au lieu de verrouiller toute la carte.
ConcurrentHashMap n'autorise pas les clés NULL ni les valeurs NULL tandis que HashMap synchronisé autorise une clé NULL.
Liens similaires
En voici quelques uns:
1) ConcurrentHashMap verrouille uniquement une partie de la carte, mais SynchronizedMap verrouille l'ensemble de MAp.
2) ConcurrentHashMap a de meilleures performances que SynchronizedMap et est plus évolutif.
3) En cas de lecteurs multiples et d’écrivain unique, ConcurrentHashMap est le meilleur choix.
Ce texte est de Différence entre ConcurrentHashMap et hashtable en Java
Nous pouvons atteindre la sécurité des threads en utilisant ConcurrentHashMap et synchronizedHashmap et Hashtable. Mais il y a beaucoup de différence si vous regardez leur architecture.
Les deux maintiendront le verrou au niveau de l'objet. Donc, si vous souhaitez effectuer une opération telle que put/get, vous devez d'abord acquérir le verrou. Dans le même temps, les autres threads ne sont autorisés à effectuer aucune opération. Donc, à la fois, un seul thread peut fonctionner sur cela. Donc, le temps d'attente augmentera ici. Nous pouvons dire que les performances sont relativement faibles lorsque vous comparez avec ConcurrentHashMap.
Il maintiendra le verrou au niveau du segment. Il comporte 16 segments et maintient le niveau de concurrence de 16 par défaut. Donc, à la fois, 16 threads peuvent fonctionner sur ConcurrentHashMap. De plus, l'opération de lecture ne nécessite pas de verrouillage. Donc, n'importe quel nombre de threads peuvent effectuer une opération get sur celui-ci.
Si thread1 veut effectuer une opération de vente dans le segment 2 et que thread2 souhaite effectuer une opération de vente sur le segment 4, il est autorisé à le faire ici. Cela signifie que 16 threads peuvent effectuer une opération de mise à jour (insertion/suppression) sur ConcurrentHashMap à la fois.
Alors que le temps d'attente sera moins ici. Par conséquent, les performances sont relativement meilleures que synchronizedHashmap et Hashtable.
ConcurrentHashMap
SynchronizedHashMap
ConcurrentHashMap est optimisé pour un accès simultané.
Les accès ne verrouillent pas la totalité de la carte mais utilisent une stratégie plus fine, ce qui améliore l'évolutivité. Il existe également des améliorations fonctionnelles spécifiquement pour l'accès simultané, par exemple. itérateurs simultanés.
Il y a une caractéristique critique à noter à propos de ConcurrentHashMap
autre que la fonctionnalité de concurrence qu'il fournit, qui est fail-safe iterator. J'ai vu des développeurs utiliser ConcurrentHashMap
simplement parce qu'ils voulaient éditer le entryset - mettre/supprimer en le parcourant. Collections.synchronizedMap(Map)
ne fournit pas fail-safe iterator mais fournit échec rapide itérateur à la place. Les itérateurs fail-fast utilisent une capture instantanée de la taille de la carte qui ne peut pas être modifiée lors de l'itération.
La méthode Collections.synchronizedMap () synchronise toutes les méthodes de HashMap et la réduit efficacement à une structure de données dans laquelle un thread peut entrer à la fois car elle verrouille chaque méthode sur un verrou commun.
Dans ConcurrentHashMap, la synchronisation se fait un peu différemment. Plutôt que de verrouiller chaque méthode sur un verrou commun, ConcurrentHashMap utilise un verrou séparé pour des compartiments séparés, ne verrouillant ainsi qu'une partie de la carte . Par défaut, il existe 16 compartiments et des verrous distincts pour les compartiments séparés. Le niveau de simultanéité par défaut est donc 16. Cela signifie théoriquement qu’un moment donné, 16 threads peuvent accéder à ConcurrentHashMap s’ils passent tous à des compartiments distincts.
En général, si vous voulez utiliser la variable ConcurrentHashMap
, assurez-vous d'être prêt à manquer les 'mises à jour'.
(c’est-à-dire que l’impression du contenu de HashMap ne garantit pas l’impression de la carte mise à jour) et utilise des API telles que CyclicBarrier
pour assurer la cohérence du cycle de vie de votre programme.
Outre ce qui a été suggéré, j'aimerais publier le code source lié à SynchronizedMap
.
Pour sécuriser un thread Map
, nous pouvons utiliser l'instruction Collections.synchronizedMap
et entrer l'instance de carte en tant que paramètre.
L'implémentation de synchronizedMap
dans Collections
est comme ci-dessous
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
Comme vous pouvez le constater, l'objet Map
en entrée est encapsulé par l'objet SynchronizedMap
.
Creusons dans l'implémentation de SynchronizedMap
,
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
public void putAll(Map<? extends K, ? extends V> map) {
synchronized (mutex) {m.putAll(map);}
}
public void clear() {
synchronized (mutex) {m.clear();}
}
private transient Set<K> keySet;
private transient Set<Map.Entry<K,V>> entrySet;
private transient Collection<V> values;
public Set<K> keySet() {
synchronized (mutex) {
if (keySet==null)
keySet = new SynchronizedSet<>(m.keySet(), mutex);
return keySet;
}
}
public Set<Map.Entry<K,V>> entrySet() {
synchronized (mutex) {
if (entrySet==null)
entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
return entrySet;
}
}
public Collection<V> values() {
synchronized (mutex) {
if (values==null)
values = new SynchronizedCollection<>(m.values(), mutex);
return values;
}
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return m.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return m.hashCode();}
}
public String toString() {
synchronized (mutex) {return m.toString();}
}
// Override default methods in Map
@Override
public V getOrDefault(Object k, V defaultValue) {
synchronized (mutex) {return m.getOrDefault(k, defaultValue);}
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
synchronized (mutex) {m.forEach(action);}
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
synchronized (mutex) {m.replaceAll(function);}
}
@Override
public V putIfAbsent(K key, V value) {
synchronized (mutex) {return m.putIfAbsent(key, value);}
}
@Override
public boolean remove(Object key, Object value) {
synchronized (mutex) {return m.remove(key, value);}
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
synchronized (mutex) {return m.replace(key, oldValue, newValue);}
}
@Override
public V replace(K key, V value) {
synchronized (mutex) {return m.replace(key, value);}
}
@Override
public V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);}
}
@Override
public V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);}
}
@Override
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
synchronized (mutex) {return m.compute(key, remappingFunction);}
}
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
synchronized (mutex) {return m.merge(key, value, remappingFunction);}
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
Ce que SynchronizedMap
fait peut être résumé en ajoutant un verrou unique à la méthode principale de l’objet Map
. Toutes les méthodes protégées par le verrou ne sont pas accessibles simultanément par plusieurs threads. Cela signifie que des opérations normales telles que put
et get
peuvent être exécutées par un seul thread en même temps pour toutes les données de l'objet Map
.
Cela rend le thread d'objet Map
sûr maintenant, mais les performances peuvent devenir un problème dans certains scénarios.
La ConcurrentMap
est beaucoup plus compliquée dans la mise en œuvre, on peut se référer à Construire un meilleur HashMap pour plus de détails. En un mot, il est mis en œuvre en tenant compte à la fois de la sécurité des threads et des performances.