java multithreading synchronized java.util.concurrent

java - ¿Es ConcurrentHashMap totalmente seguro?



multithreading synchronized (6)

este es un pasaje de JavaDoc sobre ConcurrentHashMap . Dice que las operaciones de recuperación generalmente no se bloquean, por lo que pueden superponerse con las operaciones de actualización. ¿Esto significa que el método get() no es seguro para subprocesos?

"Sin embargo, aunque todas las operaciones son seguras para subprocesos, las operaciones de recuperación no implican bloqueo, y no hay ningún soporte para bloquear toda la tabla de una manera que impida todo acceso. Esta clase es totalmente interoperable con Hashtable en programas que dependen de su seguridad de hilo pero no en sus detalles de sincronización.

Las operaciones de recuperación (incluido get) generalmente no se bloquean, por lo que pueden superponerse con las operaciones de actualización (incluso poner y eliminar). Las recuperaciones reflejan los resultados de las operaciones de actualización completadas más recientemente, que se realizan desde su inicio ".


get () en ConcurrentHashMap es seguro para subprocesos porque lee el valor que es volátil. Y en los casos en que el valor es nulo de cualquier clave, el método get () espera hasta que obtiene el bloqueo y luego lee el valor actualizado.

Cuando el método put() actualiza CHM, establece el valor de esa clave en nulo, y luego crea una nueva entrada y actualiza el CHM. Este valor nulo es utilizado por el método get() como señal de que otro hilo está actualizando el CHM con la misma clave.


El método get() es seguro para subprocesos y los otros usuarios le dieron respuestas útiles con respecto a este problema en particular.

Sin embargo, aunque ConcurrentHashMap es un reemplazo de reemplazo seguro para subprocesos para HashMap , es importante darse cuenta de que si realiza varias operaciones, es posible que tenga que cambiar el código de manera significativa. Por ejemplo, toma este código:

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

En un entorno de múltiples hilos, esta es una condición de carrera. Debe usar el ConcurrentHashMap.putIfAbsent(K key, V value) y prestar atención al valor de retorno, que le indica si la operación put fue exitosa o no. Lea los documentos para más detalles.

Respondiendo a un comentario que pide una aclaración sobre por qué esta es una condición de carrera.

Imagine que hay dos hilos A , B que van a poner dos valores diferentes en el mapa, v1 y v2 respectivamente, que tienen la misma clave. La clave inicialmente no está presente en el mapa. Se entrelazan de esta manera:

  • El subproceso A llama containsKey y descubre que la clave no está presente, pero se suspende inmediatamente.
  • El subproceso B llama a containsKey y descubre que la clave no está presente y tiene tiempo para insertar su valor v2 .
  • El subproceso A reanuda e inserta v1 , sobrescribiendo "pacíficamente" (puesto que put es threadsafe) el valor insertado por el subproceso B

Ahora el hilo B "piensa" que ha insertado con éxito su propio valor v2 , pero el mapa contiene v1 . Esto es realmente un desastre porque el hilo B puede llamar a v2.updateSomething() y "pensar" que los consumidores del mapa (por ejemplo, otros hilos) tienen acceso a ese objeto y verá que tal vez la actualización importante ("como: este visitante IP dirección está tratando de realizar un DOS, rechazar todas las solicitudes a partir de ahora "). En cambio, el objeto pronto será recolectado y extraído.


Es seguro para subprocesos. Sin embargo, la forma en que está siendo seguro para subprocesos puede no ser lo que esperas. Hay algunos "consejos" que puede ver en:

Esta clase es completamente interoperable con Hashtable en programas que se basan en la seguridad de sus hilos pero no en sus detalles de sincronización

Para conocer la historia completa en una imagen más completa, debe conocer la interfaz de ConcurrentMap .

El Map original proporciona algunos métodos de lectura / actualización muy básicos. Incluso pude hacer una implementación segura de subprocesos de Map ; Hay muchos casos en que las personas no pueden usar mi Mapa sin considerar mi mecanismo de sincronización. Este es un ejemplo típico:

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

Este fragmento de código no es seguro para subprocesos, aunque el mapa sí lo es. Dos hilos que llaman containsKey() al mismo tiempo podrían pensar que no existe tal clave que ambos inserten en el Map .

Para solucionar el problema, necesitamos hacer una sincronización extra explícitamente. Supongamos que la seguridad de subprocesos de mi Mapa se logra mediante palabras clave sincronizadas, tendrá que hacer:

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

Tal código adicional necesita que conozca los "detalles de sincronización" del mapa. En el ejemplo anterior, necesitamos saber que la sincronización se logra por "sincronizado".

ConcurrentMap interfaz ConcurrentMap lleva esto un paso más allá. Define algunas acciones "complejas" comunes que implican acceso múltiple al mapa. Por ejemplo, el ejemplo anterior se expone como putIfAbsent() . Con estas acciones "complejas", los usuarios de ConcurrentMap (en la mayoría de los casos) no necesitan sincronizar las acciones con acceso múltiple al mapa. Por lo tanto, la implementación de Map puede realizar un mecanismo de sincronización más complicado para un mejor rendimiento. ConcurrentHashhMap es un buen ejemplo. La seguridad del subproceso se mantiene de hecho manteniendo bloqueos separados para las diferentes particiones del mapa. Es seguro para subprocesos porque el acceso simultáneo al mapa no dañará la estructura de datos interna, ni ocasionará la pérdida inesperada de la actualización, etc.

Con todo lo anterior en mente, el significado de Javadoc será más claro:

"Las operaciones de recuperación (incluido get) generalmente no se bloquean" porque ConcurrentHashMap no está utilizando "sincronizado" para su seguridad de subprocesos. La lógica de get se ocupa de la seguridad de los hilos; y si miras más lejos en el Javadoc:

La tabla está particionada internamente para intentar permitir el número indicado de actualizaciones concurrentes sin contención

No solo la recuperación no es un bloqueo, incluso las actualizaciones pueden ocurrir al mismo tiempo. Sin embargo, no-blocking / concurrent-updates no significa que sea thread-Unsafe. Simplemente significa que está usando algunas formas que no son simples "sincronizadas" para seguridad de hilos.

Sin embargo, como el mecanismo de sincronización interna no está expuesto, si desea realizar algunas acciones complicadas distintas a las proporcionadas por ConcurrentMap , es posible que deba considerar cambiar su lógica o considerar no usar ConcurrentHashMap . Por ejemplo:

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


Simplemente significa que cuando un hilo se está actualizando y un hilo está leyendo, no hay garantía de que el que llamó al método ConcurrentHashMap primero, a tiempo, tenga su operación primero.

Piense en una actualización del artículo que dice dónde está Bob. Si un hilo pregunta dónde está Bob aproximadamente al mismo tiempo que otro hilo se actualiza para decir que entró "adentro", no puede predecir si el hilo del lector obtendrá el estado de Bob como "dentro" o "afuera". Incluso si el hilo de actualización llama primero al método, el hilo del lector podría obtener el estado ''exterior''.

Los hilos no se causarán problemas entre sí. El código es ThreadSafe.

Un hilo no entrará en un bucle infinito ni comenzará a generar NullPointerExceptions extrañas ni obtendrá "itside" con la mitad del antiguo estado y la mitad del nuevo.


ConcurrentHashmap.get() es seguro para subprocesos, en el sentido de que

  • No lanzará ninguna excepción, incluida ConcurrentModificationException
  • Devolverá un resultado que fue verdadero en algún tiempo (reciente) en el pasado. Esto significa que dos llamadas consecutivas para obtener pueden arrojar resultados diferentes. Por supuesto, esto es cierto de cualquier otro Map también.

HashMap está dividido en "buckets" basados ​​en hashCode . ConcurrentHashMap usa este hecho. Su mecanismo de sincronización se basa en cubos de bloqueo en lugar de en todo el Map . De esta forma, pocos hilos pueden escribir simultáneamente en algunos cubos diferentes (un hilo puede escribir en un cubo a la vez).

Leer desde ConcurrentHashMap casi no usa mecanismos de sincronización. Dije casi porque usa la sincronización cuando obtendrá un valor null . Basado en el hecho de que ConcurrentHashMap no puede almacenar valores null (sí, los valores tampoco pueden ser nulos), si obtiene ese valor debe significar que la lectura fue invocada en el medio del par clave-valor de escritura (después de que la entrada fue creada para clave, pero su valor aún no se había establecido; era null ). En tal caso, el hilo de lectura deberá esperar hasta que la escritura haya finalizado.

Por lo tanto, los resultados de read() se basarán en el estado actual del mapa. Si lee el valor de la clave que estaba en el medio de la actualización, es probable que obtenga un valor anterior, ya que el proceso de escritura aún no ha finalizado.