Es seguro obtener valores de un java.útil.HashMap de múltiples hilos (sin modificación)?


Hay un caso en el que se construirá un mapa, y una vez que se inicialice, nunca se modificará de nuevo. Sin embargo, se accederá (solo a través de get(key)) desde múltiples subprocesos. ¿Es seguro usar un java.util.HashMap de esta manera?

(Actualmente, estoy felizmente usando un java.util.concurrent.ConcurrentHashMap, y no tengo una necesidad medida de mejorar el rendimiento, pero simplemente tengo curiosidad si un simple HashMap sería suficiente. Por lo tanto, esta pregunta es no "¿Cuál debo usar?"tampoco es una cuestión de rendimiento. Más bien, el la pregunta es " ¿Sería seguro?")

Author: Dave L., 2008-09-19

11 answers

Tu modismo es seguro si y solo si la referencia a HashMap es publicada de forma segura. En lugar de cualquier cosa relacionada con las partes internas de HashMap en sí, safe publication trata de cómo el hilo de construcción hace que la referencia al mapa sea visible para otros hilos.

Básicamente, la única carrera posible aquí es entre la construcción del HashMap y cualquier hilo de lectura que pueda acceder a él antes de que esté completamente construido. La mayor parte de la discusión es sobre lo que sucede con el estado del objeto map, pero esto es irrelevante ya que nunca lo modifica, por lo que la única parte interesante es cómo se publica la referencia HashMap.

Por ejemplo, imagine que publica el mapa de esta manera:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... y en algún momento setMap() se llama con un mapa, y otros hilos están usando SomeClass.MAP para acceder al mapa, y comprobar si hay null como este:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

Esto es no es seguro aunque probablemente parezca que lo es. El el problema es que no haysucede-antes relación entre el conjunto de SomeObject.MAP y la lectura posterior en otro hilo, por lo que el hilo de lectura es libre para ver un mapa parcialmente construido. Esto puede hacer bastante cualquier cosa e incluso en la práctica hace cosas como poner el hilo de lectura en un bucle infinito.

Para publicar de forma segura el mapa, es necesario establecer un sucede-antes de relación entre el escritura de la referencia a la HashMap (es decir, la publicación ) y los lectores posteriores de esa referencia (es decir, el consumo). Convenientemente, hay solo unas pocas maneras fáciles de recordar para lograr que[1]:

  1. Intercambie la referencia a través de un campo correctamente bloqueado (JLS 17.4.5)
  2. Use el inicializador estático para hacer los almacenes de inicialización (JLS 12.4)
  3. Intercambie la referencia a través de un campo volátil (JLS 17.4.5 ), o como consecuencia de esta regla, a través de las clases AtomicX
  4. Inicializa el valor en un campo final ( JLS 17.5).

Los más interesantes para su escenario son (2), (3) y (4). En particular, (3) se aplica directamente al código que tengo arriba: si transformas la declaración de MAP a:

public static volatile HashMap<Object, Object> MAP;

Entonces todo es kosher: los lectores que ven un valor no nulo necesariamente tienen una sucede-antes de relación con el almacén a MAP y por lo tanto ver todos los almacenes asociados con la inicialización del mapa.

Los otros métodos cambian la semántica de su método, ya que tanto (2) (usando el inicializador estático) como (4) (usando final) implican que no puede establecer MAP dinámicamente en tiempo de ejecución. Si no necesita para hacer eso, entonces simplemente declare MAP como static final HashMap<> y se le garantiza una publicación segura.

En la práctica, las reglas son simples para el acceso seguro a "objetos nunca modificados":

Si está publicando un objeto que no es inherentemente inmutable (como en todos los campos declarados final) y:

  • Ya puede crear el objeto que se asignará en el momento de la declaracióna: solo use un campo final (incluyendo static final para miembros estáticos).
  • Desea asignar el objeto más tarde, después de que la referencia ya sea visible: use un campo volátil b.

Eso es it!

En la práctica, es muy eficiente. El uso de un campo static final, por ejemplo, permite a la JVM asumir que el valor no cambia durante la vida útil del programa y optimizarlo en gran medida. El uso de un campo miembro finalpermite a la mayoría de las arquitecturasleer el campo de una manera equivalente a una lectura normal del campo y no inhibe más optimizaciones c.

Finalmente, el uso de volatile tiene cierto impacto: no se necesita ninguna barrera de hardware en muchas arquitecturas (como x86, específicamente aquellos que no permiten lecturas para pasar lecturas), pero algunas optimizaciones y reordenamientos pueden no ocurrir en tiempo de compilación, pero este efecto es generalmente pequeño. A cambio, en realidad obtienes más de lo que pediste - no solo puedes publicar de forma segura una HashMap, puedes almacenar tantas HashMaps no modificadas como quieras a la misma referencia y estar seguro de que todos los lectores verán un mapa publicado de forma segura.

Para más detalles sangrientos, refiérase a Shipilev o esta FAQ de Manson y Goetz .


[1] Citando directamente a shipilev.


a Eso suena complicado, pero lo que quiero decir es que puede asignar la referencia en el tiempo de construcción, ya sea en el punto de declaración o en el constructor (campos miembro) o inicializador estático (campos estáticos).

b Opcionalmente, puede usar un método synchronized para obtener / establecer, o un AtomicReference o algo, pero estamos hablando de la trabajo mínimo que puedes hacer.

C Algunas arquitecturas con modelos de memoria muy débiles (estoy mirando usted, Alfa) pueden requerir algún tipo de barrera de lectura antes de una lectura final - pero estos son muy raros hoy en día.

 27
Author: BeeOnRope,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:29

Jeremy Manson, el dios cuando se trata del Modelo de Memoria Java, tiene un blog de tres partes sobre este tema - porque en esencia usted está haciendo la pregunta "Es seguro acceder a un HashMap inmutable" - la respuesta a eso es sí. Pero debe responder el predicado a esa pregunta que es - "Es mi HashMap inmutable". La respuesta podría sorprenderte - Java tiene un conjunto relativamente complicado de reglas para determinar la inmutabilidad.

Para obtener más información sobre el tema, lea el blog de Jeremy mensajes:

Parte 1 sobre la inmutabilidad en Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html

Parte 2 sobre la inmutabilidad en Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

Parte 3 sobre la inmutabilidad en Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html

 70
Author: Taylor Gautier,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-19 18:56:36

Las lecturas son seguras desde el punto de vista de la sincronización, pero no desde el punto de vista de la memoria. Esto es algo que es ampliamente mal entendido entre los desarrolladores de Java, incluyendo aquí en Stackoverflow. (Observe la calificación de esta respuesta como prueba.)

Si tiene otros subprocesos en ejecución, es posible que no vean una copia actualizada del HashMap si no hay escritura de memoria fuera del subproceso actual. Las escrituras de memoria se producen mediante el uso de palabras clave sincronizadas o volátiles, o mediante el uso de algunas construcciones de simultaneidad de Java.

Ver El artículo de Brian Goetz sobre el nuevo Modelo de Memoria Java para más detalles.

 34
Author: Heath Borders,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:29

Después de mirar un poco más, encontré esto en el java doc (énfasis mío):

Tenga en cuenta que esta implementación no es sincronizar. Si hay varios hilos acceder a un mapa hash simultáneamente, y en al menos uno de los hilos modifica el mapa estructural, debe ser sincronizado externamente. (Una estructura modificación es cualquier operación que añade o elimina una o más asignaciones; simplemente cambiando el valor asociado con una clave que una instancia ya contiene no es una estructura modificación.)

Esto parece implicar que será seguro, asumiendo que el inverso de la declaración allí es verdadero.

 10
Author: Dave L.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-19 18:20:40

Una nota es que bajo algunas circunstancias, un get() de un HashMap no sincronizado puede causar un bucle infinito. Esto puede ocurrir si un put () simultáneo causa una repetición del mapa.

Http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

 9
Author: Alex Miller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-19 21:13:09

Sin embargo, hay un giro importante. Es seguro acceder al mapa, pero en general no se garantiza que todos los subprocesos verán exactamente el mismo estado (y por lo tanto los valores) del HashMap. Esto puede suceder en sistemas multiprocesadores donde las modificaciones al HashMap hechas por un subproceso (por ejemplo, el que lo llenó) pueden estar en la caché de esa CPU y no serán vistas por subprocesos que se ejecutan en otras CPU, hasta que se realice una operación de valla de memoria que garantice la coherencia de la caché. El Lenguaje Java La especificación es explícita en este caso: la solución es adquirir un bloqueo (sincronizado (...)) que emite una operación de valla de memoria. Por lo tanto, si está seguro de que después de rellenar el HashMap cada uno de los hilos adquiere CUALQUIER bloqueo, entonces está bien a partir de ese punto para acceder al HashMap desde cualquier hilo hasta que el HashMap se modifica de nuevo.

 8
Author: Alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-19 19:38:48

Según http://www.ibm.com/developerworks/java/library/j-jtp03304 / # Seguridad de inicialización puede hacer que su HashMap sea un campo final y después de que el constructor termine, se publicará de forma segura.

... Bajo el nuevo modelo de memoria, hay algo similar a una relación sucede-antes entre la escritura de un campo final en un constructor y la carga inicial de una referencia compartida a ese objeto en otro hilo. ...

 4
Author: bodrin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-30 16:08:32

Así que el escenario que estás describiendo es que necesitas poner un montón de datos en un mapa, luego cuando termines de poblarlo lo tratas como inmutable. Un enfoque que es "seguro" (lo que significa que está imponiendo que realmente se trata como inmutable) es reemplazar la referencia con Colecciones.Mapa no modificable (mapa original) cuando esté listo para hacerlo inmutable.

Para ver un ejemplo de lo mal que los mapas pueden fallar si se usan simultáneamente, y la solución sugerida que mencioné, echa un vistazo a esto entrada del desfile de errores: bug_id=6423457

 3
Author: Will,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-09 16:37:06

Ser advertido de que, incluso en un único subproceso código, la sustitución de un ConcurrentHashMap con un HashMap pueden no ser seguros. ConcurrentHashMap prohíbe null como clave o valor. HashMap no les prohíbe (no pregunte).

Por lo tanto, en la improbable situación de que su código existente pueda agregar un null a la colección durante la configuración (presumiblemente en un caso de error de algún tipo), reemplazar la colección como se describe cambiará el comportamiento funcional.

Dicho esto, siempre que no hagas nada más las lecturas simultáneas de un HashMap son seguras.

[Editar: por "lecturas concurrentes", quiero decir que no hay también modificaciones concurrentes.

Otras respuestas explican cómo asegurar esto. Una forma es hacer que el mapa sea inmutable, pero no es necesario. Por ejemplo, el modelo de memoria JSR133 define explícitamente iniciar un subproceso como una acción sincronizada, lo que significa que los cambios realizados en el subproceso A antes de que inicie el subproceso B son visibles en el subproceso B.

Mi intención no es contradecir a los respuestas más detalladas sobre el Modelo de Memoria Java. Esta respuesta tiene la intención de señalar que incluso aparte de los problemas de concurrencia, hay al menos una diferencia de API entre ConcurrentHashMap y HashMap, que podría echar por tierra incluso un programa de un solo subproceso que reemplazó a uno con el otro.]

 1
Author: Steve Jessop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-07-07 16:44:54

Http://www.docjar.com/html/api/java/util/HashMap.java.html

Aquí está la fuente para HashMap. Como puede ver, no hay absolutamente ningún código de bloqueo / mutex allí.

Esto significa que si bien está bien leer desde un HashMap en una situación multiproceso, definitivamente usaría un ConcurrentHashMap si hubiera varias escrituras.

Lo interesante es que tanto la tabla HashTable.NET como el Diccionario han incorporado código de sincronización.

 0
Author: FlySwat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-19 18:19:21

Si la inicialización y cada put está sincronizada, está guardado.

El siguiente código se guarda porque el classloader se encargará de la sincronización:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

El siguiente código se guarda porque la escritura de volatile se encargará de la sincronización.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

Esto también funcionará si la variable miembro es final porque final también es volátil. Y si el método es un constructor.

 0
Author: TomWolk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-28 11:38:27