example - java hashmap thread safe
¿Cuál es la diferencia entre ConcurrentHashMap y Collections.synchronizedMap(Mapa)? (18)
Tengo un mapa que se va a modificar por varios hilos al mismo tiempo.
Parece que hay tres implementaciones de mapas sincronizados diferentes en la API de Java:
Hashtable
-
Collections.synchronizedMap(Map)
-
ConcurrentHashMap
Por lo que entiendo, Hashtable
es una implementación antigua (que extiende la clase de Dictionary
obsoleta), que se adaptó posteriormente para que se ajuste a la interfaz del Map
. Si bien está sincronizado, parece tener serios problemas de escalabilidad y no se recomienda para nuevos proyectos.
Pero ¿qué pasa con los otros dos? ¿Cuáles son las diferencias entre los mapas devueltos por Collections.synchronizedMap(Map)
y ConcurrentHashMap
s? ¿Cuál encaja en qué situación?
- Si la consistencia de los datos es muy importante, use Hashtable o Collections.synchronizedMap (Mapa).
- Si la velocidad / el rendimiento es muy importante y la actualización de datos puede verse comprometida, use ConcurrentHashMap.
Además de lo que se ha sugerido, me gustaría publicar el código fuente relacionado con SynchronizedMap
.
Para hacer seguro un hilo de Map
, podemos usar la declaración Collections.synchronizedMap
e ingresar la instancia del mapa como parámetro.
La implementación de synchronizedMap
en Collections
es como abajo
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
Como puede ver, el objeto de Map
entrada está envuelto por el objeto SynchronizedMap
.
Vamos a profundizar en la implementación 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();}
}
}
Lo que hace SynchronizedMap
puede resumirse como agregar un solo bloqueo al método primario del objeto Map
entrada. No se puede acceder a todos los métodos protegidos por el bloqueo por varios subprocesos al mismo tiempo. Eso significa que las operaciones normales, como put
y get
pueden ser ejecutadas por un solo hilo al mismo tiempo para todos los datos en el objeto Map
.
Hace que el subproceso del objeto Map
ahora sea seguro, pero el rendimiento puede convertirse en un problema en algunos escenarios.
El ConcurrentMap
es mucho más complicado en la implementación, podemos consultar Crear un mejor HashMap para obtener más detalles. En pocas palabras, se implementa teniendo en cuenta la seguridad y el rendimiento del subproceso.
Aquí hay algunos:
1) ConcurrentHashMap bloquea solo la parte de Map pero SynchronizedMap bloquea MAp completo.
2) ConcurrentHashMap tiene un mejor rendimiento sobre SynchronizedMap y más escalable.
3) En el caso de un lector múltiple y un solo escritor, ConcurrentHashMap es la mejor opción.
Este texto es de la diferencia entre ConcurrentHashMap y hashtable en Java
ConcurrentHashMap está optimizado para el acceso concurrente.
Los accesos no bloquean todo el mapa, sino que utilizan una estrategia más precisa, lo que mejora la escalabilidad. También hay mejoras funcionales específicas para el acceso concurrente, por ejemplo, iteradores concurrentes.
El método Collections.synchronizedMap () sincroniza todos los métodos de HashMap y lo reduce efectivamente a una estructura de datos donde un hilo puede ingresar a la vez porque bloquea todos los métodos en un bloqueo común.
En ConcurrentHashMap la sincronización se realiza de forma un poco diferente. En lugar de bloquear todos los métodos en un bloqueo común, ConcurrentHashMap usa un bloqueo separado para cubos separados, bloqueando así solo una parte del Mapa. Por defecto, hay 16 cubos y también bloqueos separados para cubos separados. Por lo tanto, el nivel de concurrencia predeterminado es 16. Eso significa que, en teoría, en cualquier momento dado, 16 subprocesos pueden acceder a ConcurrentHashMap si todos van a separar las categorías.
En ConcurrentHashMap
, el bloqueo se aplica a un segmento en lugar de a un Mapa completo. Cada segmento gestiona su propia tabla hash interna. El bloqueo se aplica solo para las operaciones de actualización. Collections.synchronizedMap(Map)
sincroniza todo el mapa.
En general, si desea utilizar el ConcurrentHashMap
asegúrese de estar listo para omitir las ''actualizaciones''
(es decir, la impresión de contenidos de HashMap no garantiza que imprimirá el Mapa actualizado) y que use API como CyclicBarrier
para garantizar la coherencia en todo el ciclo de vida de su programa.
Hay una característica crítica a tener en cuenta acerca de ConcurrentHashMap
además de la característica de concurrencia que proporciona, que es un iterador a prueba de fallas . He visto desarrolladores que usan ConcurrentHashMap
solo porque quieren editar el conjunto de entrada: poner / quitar mientras se iteran sobre él. Collections.synchronizedMap(Map)
no proporciona un iterador a prueba de fallas, sino que proporciona un iterador a prueba de fallas en su lugar. los iteradores a prueba de fallos utilizan una instantánea del tamaño del mapa que no se puede editar durante la iteración.
La principal diferencia entre estos dos es que ConcurrentHashMap
bloqueará solo una parte de los datos que se están actualizando, mientras que otros subprocesos pueden acceder a otra parte de los datos. Sin embargo, Collections.synchronizedMap()
bloqueará todos los datos durante la actualización, otros subprocesos solo podrán acceder a los datos cuando se libere el bloqueo. Si hay muchas operaciones de actualización y una cantidad relativamente pequeña de operaciones de lectura, debe elegir ConcurrentHashMap
.
También otra diferencia es que ConcurrentHashMap
no conservará el orden de los elementos en el Mapa pasado. Es similar a HashMap
cuando almacena datos. No hay garantía de que el orden del elemento se conserve. Si bien Collections.synchronizedMap()
conservará el orden de los elementos del Mapa pasado. Por ejemplo, si pasa un TreeMap
a ConcurrentHashMap
, el orden de los elementos en el ConcurrentHashMap
puede no ser el mismo que el orden en el TreeMap
, pero Collections.synchronizedMap()
conservará el orden.
Además, ConcurrentHashMap
puede garantizar que no se ConcurrentModificationException
mientras un hilo está actualizando el mapa y otro está atravesando el iterador obtenido del mapa. Sin embargo, Collections.synchronizedMap()
no está garantizado en esto.
Hay una publicación que demuestra las diferencias de estos dos y también el ConcurrentSkipListMap
.
Los "problemas de escalabilidad" para Hashtable
están presentes exactamente de la misma forma que en Collections.synchronizedMap(Map)
: usan una sincronización muy simple, lo que significa que solo un hilo puede acceder al mapa al mismo tiempo.
Esto no es un gran problema cuando tiene inserciones y búsquedas simples (a menos que lo haga con mucha intensidad), pero se convierte en un gran problema cuando necesita recorrer todo el Mapa, lo que puede llevar mucho tiempo para un Mapa grande, mientras Un hilo hace eso, todos los demás tienen que esperar si quieren insertar o buscar algo.
El ConcurrentHashMap
utiliza técnicas muy sofisticadas para reducir la necesidad de sincronización y permitir el acceso de lectura en paralelo por varios subprocesos sin sincronización y, lo que es más importante, proporciona un Iterator
que no requiere sincronización e incluso permite que el mapa se modifique durante la interacción (aunque no ofrece garantías si se devolverán o no los elementos que se insertaron durante la iteración).
Para sus necesidades, use ConcurrentHashMap
. Permite la modificación concurrente del Mapa desde varios hilos sin la necesidad de bloquearlos. Collections.synchronizedMap(map)
crea un Mapa de bloqueo que degradará el rendimiento, aunque garantiza la coherencia (si se usa correctamente).
Use la segunda opción si necesita asegurar la consistencia de los datos, y cada hilo debe tener una vista actualizada del mapa. Use el primero si el rendimiento es crítico, y cada hilo solo inserta datos en el mapa, con lecturas que ocurren con menos frecuencia.
Podemos lograr la seguridad de subprocesos utilizando ConcurrentHashMap y synchronisedHashmap and Hashtable. Pero hay mucha diferencia si miras su arquitectura.
- Hashmap sincronizado y hashtable
Ambos mantendrán el bloqueo en el nivel del objeto. Entonces, si desea realizar cualquier operación como poner / obtener, primero debe adquirir el bloqueo. Al mismo tiempo, otros hilos no tienen permitido realizar ninguna operación. Así que a la vez, solo un hilo puede operar en esto. Así que el tiempo de espera aumentará aquí. Podemos decir que el rendimiento es relativamente bajo cuando se compara con ConcurrentHashMap.
- HashMap concurrente
Mantendrá el bloqueo a nivel de segmento. Tiene 16 segmentos y mantiene el nivel de concurrencia como 16 por defecto. Entonces, a la vez, 16 hilos pueden funcionar en ConcurrentHashMap. Por otra parte, la operación de lectura no requiere un bloqueo. Así que cualquier cantidad de hilos puede realizar una operación de obtención en él.
Si el hilo 1 quiere realizar una operación de venta en el segmento 2 y el hilo 2 quiere realizar una operación de venta en el segmento 4, entonces se permite aquí. Significa que 16 hilos pueden realizar operaciones de actualización (poner / eliminar) en ConcurrentHashMap a la vez.
Para que el tiempo de espera sea menor aquí. Por lo tanto, el rendimiento es relativamente mejor que el mapa sincronizado de Hash y Hashtable.
Se prefiere ConcurrentHashMap cuando se puede usar, aunque requiere al menos Java 5.
Está diseñado para escalar bien cuando es usado por múltiples hilos. El rendimiento puede ser marginalmente más bajo cuando solo un solo hilo accede al Mapa a la vez, pero es significativamente mejor cuando múltiples hilos acceden al mapa simultáneamente.
Encontré una entrada de blog que reproduce una tabla del excelente libro Java Concurrency In Practice , que recomiendo ampliamente.
Collections.synchronizedMap solo tiene sentido si necesita envolver un mapa con algunas otras características, tal vez algún tipo de mapa ordenado, como un TreeMap.
Tienes razón sobre HashTable
, puedes olvidarlo.
Su artículo menciona el hecho de que mientras HashTable y la clase de envoltura sincronizada brindan seguridad de subprocesos básica al permitir que solo un subproceso a la vez accedan al mapa, esto no es "seguro" de subprocesos ya que muchas operaciones compuestas aún requieren sincronización adicional, por ejemplo. ejemplo:
synchronized (records) {
Record rec = records.get(id);
if (rec == null) {
rec = new Record(id);
records.put(id, rec);
}
return rec;
}
Sin embargo, no piense que ConcurrentHashMap
es una alternativa simple para un HashMap
con un bloque synchronized
típico como se muestra arriba. Lee this artículo para entender mejor sus complejidades.
HashMap concurrente
- Debe usar ConcurrentHashMap cuando necesite una concurrencia muy alta en su proyecto.
- Es seguro para subprocesos sin sincronizar todo el mapa.
- Las lecturas pueden suceder muy rápido, mientras que la escritura se realiza con un bloqueo.
- No hay bloqueo en el nivel del objeto.
- El bloqueo se encuentra en una granularidad mucho más fina a nivel de cubo hashmap.
- ConcurrentHashMap no lanza una ConcurrentModificationException si un subproceso intenta modificarlo mientras otro está iterando sobre él.
- ConcurrentHashMap utiliza multitud de bloqueos.
SynchronizedHashMap
- Sincronización a nivel de objeto.
- Cada operación de lectura / escritura necesita adquirir bloqueo.
- Bloquear toda la colección es una sobrecarga de rendimiento.
- Básicamente, esto da acceso a un solo hilo a todo el mapa y bloquea todos los otros hilos.
- Puede causar contención.
- SynchronizedHashMap devuelve Iterator, que falla rápidamente en la modificación concurrente.
Mapa sincronizado:
El mapa sincronizado tampoco es muy diferente de Hashtable y proporciona un rendimiento similar en programas Java simultáneos. La única diferencia entre Hashtable y SynchronizedMap es que SynchronizedMap no es un legado y puede envolver cualquier Map para crear su versión sincronizada usando el método Collections.synchronizedMap ().
HashMap concurrente:
La clase ConcurrentHashMap proporciona una versión concurrente del HashMap estándar. Esto es una mejora en la funcionalidad synchronizedMap proporcionada en la clase Colecciones.
A diferencia de Hashtable y Synchronized Map, nunca bloquea todo el mapa, sino que divide el mapa en segmentos y el bloqueo se realiza en ellos. Se realiza mejor si el número de subprocesos del lector es mayor que el número de subprocesos de escritura.
ConcurrentHashMap se divide de forma predeterminada en 16 regiones y se aplican los bloqueos. Este número predeterminado se puede establecer al inicializar una instancia de ConcurrentHashMap. Al configurar los datos en un segmento en particular, se obtiene el bloqueo para ese segmento. Esto significa que dos actualizaciones pueden ejecutarse de manera simultánea y segura si cada una afecta a grupos separados, lo que minimiza la contención de bloqueo y maximiza el rendimiento.
ConcurrentHashMap no lanza una excepción concurrenteModificationException
ConcurrentHashMap no lanza una excepción ConcurrentModificationException si un subproceso intenta modificarlo mientras otro está iterando sobre él
Diferencia entre SynchornizedMap y ConcurrentHashMap
Collections.synchornizedMap (HashMap) devolverá una colección que es casi equivalente a Hashtable, donde cada operación de modificación en el Mapa se bloquea en el objeto Map, mientras que en el caso de ConcurrentHashMap, la seguridad de subprocesos se logra dividiendo el mapa completo en una partición diferente según el nivel de concurrencia y solo bloquear una parte en particular en lugar de bloquear todo el Mapa.
ConcurrentHashMap no permite claves nulas o valores nulos, mientras que HashMap sincronizado permite claves nulas.
Enlaces similares
╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗
║ 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 ║
╚═══════════════╩═══════════════════════════════════════╩═════════════════════╝
Respecto al mecanismo de bloqueo: Hashtable
bloquea el objeto , mientras que ConcurrentHashMap
bloquea solo el contenedor .
Hashtable
yConcurrentHashMap
no permiten clavesnull
o valoresnull
.Collections.synchronizedMap(Map)
sincroniza todas las operaciones (get
,put
,size
, etc.).ConcurrentHashMap
admite la concurrencia total de las recuperaciones y la concurrencia esperada ajustable para las actualizaciones.
Como de costumbre, hay concurrencia - sobrecarga - compensaciones de velocidad involucradas. Realmente debe considerar los requisitos detallados de concurrencia de su aplicación para tomar una decisión y luego probar su código para ver si es lo suficientemente bueno.