¿Es seguro obtener valores de un java.util.HashMap de varios hilos (sin modificación)?

Hay un caso donde se construirá un mapa, y una vez que se inicializa, nunca se modificará nuevamente. Sin embargo, se accederá (a través de get (key) solamente) desde múltiples hilos. ¿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 me da curiosidad si un simple HashMap sería suficiente. Por lo tanto, esta pregunta no es “¿Cuál debería usar? “ni es una pregunta de rendimiento. Más bien, la pregunta es” ¿Sería seguro? “)

Su expresión idiomática es segura si y solo si la referencia a HashMap se publica con seguridad . En lugar de todo lo relacionado con las HashMap internas de HashMap , la publicación segura se refiere a 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 de HashMap y cualquier hilo de lectura que pueda acceder antes de que esté completamente construido. La mayor parte del debate se refiere a lo que sucede con el estado del objeto del mapa, pero esto es irrelevante ya que nunca lo modifica, por lo que la única parte interesante es cómo se publica la referencia de HashMap .

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

 class SomeClass { public static HashMap MAP; public synchronized static setMap(HashMap m) { MAP = m; } } 

… y en algún momento se llama a setMap() con un mapa, y otros hilos están usando SomeClass.MAP para acceder al mapa, y verifican null así:

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

Esto no es seguro , aunque probablemente parezca como si lo fuera. El problema es que no hay una relación de SomeObject.MAP previa entre el conjunto de SomeObject.MAP y la posterior lectura en otro subproceso, por lo que el hilo de lectura es libre de ver un mapa parcialmente construido. Esto puede hacer prácticamente cualquier cosa e incluso en la práctica hace cosas como poner el hilo de lectura en un ciclo infinito .

Para publicar el mapa de forma segura, debe establecer una relación de pase previo entre la escritura de la referencia a HashMap (es decir, la publicación ) y los lectores posteriores de esa referencia (es decir, el consumo). Convenientemente, solo hay algunas maneras fáciles de recordar para lograr eso [1] :

  1. Intercambie la referencia a través de un campo bloqueado correctamente ( JLS 17.4.5 )
  2. Use el inicializador estático para hacer las tiendas de inicialización ( JLS 12.4 )
  3. Cambie 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 de AtomicX
  4. Inicialice 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 transforma la statement de MAP en:

 public static volatile HashMap MAP; 

entonces todo es kosher: los lectores que ven un valor no nulo necesariamente tienen una relación de pasar antes con la tienda a MAP y, por lo tanto, ver todas las tiendas asociadas con la inicialización del mapa.

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

En la práctica, las reglas son simples para un acceso seguro a “objetos nunca modificados”:

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

  • Ya puede crear el objeto que se le asignará en el momento de la statement a : simplemente use un campo final (incluida static final estática para miembros estáticos).
  • Desea asignar el objeto más tarde, después de que la referencia ya esté visible: use un campo volátil b .

¡Eso es!

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 se modifica durante la vida del progtwig y optimizarlo en gran medida. El uso de un campo de miembro final permite que la mayoría de las architectures lean el campo de una manera equivalente a una lectura de campo normal y no inhibe otras optimizaciones c .

Finalmente, el uso de volatile tiene cierto impacto: no se necesita barrera de hardware en muchas architectures (como x86, específicamente aquellas que no permiten que las lecturas pasen lecturas), pero es posible que no ocurra cierta optimización y reordenamiento en tiempo de comstackción, pero este efecto es generalmente pequeño. A cambio, obtienes más de lo que pedías, no solo puedes publicar un HashMap forma segura, puedes almacenar tantos HashMap más no modificados como quieras para la misma referencia y estar seguro de que todos los lectores verán de forma segura mapa publicado

Para obtener más detalles sangrientos, consulte Shipilev o estas preguntas frecuentes por Manson y Goetz .


[1] Citando directamente de shipilev .


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

b Opcionalmente, puede usar un método synchronized para obtener / configurar, o una AtomicReference o algo así, pero estamos hablando del trabajo mínimo que puede hacer.

c Algunas architectures con modelos de memoria muy débiles (te estoy mirando, Alpha) pueden requerir algún tipo de barrera de lectura antes de una lectura final , pero hoy son muy raras.

Jeremy Manson, el dios en lo que respecta al Modelo de Memoria de Java, tiene un blog de tres partes sobre este tema, porque en esencia se está preguntando “¿Es seguro acceder a un HashMap inmutable?”, La respuesta es sí. Pero debes responder al predicado de esa pregunta que es: “Mi HashMap es inmutable”. La respuesta puede sorprenderlo: Java tiene un conjunto relativamente complicado de reglas para determinar la inmutabilidad.

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

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

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

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

Las lecturas son seguras desde el punto de vista de la sincronización pero no desde la memoria. Esto es algo ampliamente malentendido entre los desarrolladores de Java, incluido 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 de HashMap si no hay memoria escrita 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.

Vea el artículo de Brian Goetz sobre el nuevo modelo de memoria de Java para más detalles.

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

Tenga en cuenta que esta implementación no está sincronizada. Si varios subprocesos acceden a un mapa hash simultáneamente, y al menos uno de los subprocesos modifica estructuralmente el mapa, debe estar sincronizado externamente. (Una modificación estructural es cualquier operación que agrega o elimina una o más asignaciones, simplemente cambiar el valor asociado con una clave que una instancia ya contiene no es una modificación estructural).

Esto parece implicar que será seguro, suponiendo que lo contrario de lo dicho sea cierto.

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

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

Sin embargo, hay un giro importante. Es seguro acceder al mapa, pero en general no se garantiza que todos los hilos vean exactamente el mismo estado (y por lo tanto los valores) de HashMap. Esto podría ocurrir en sistemas multiprocesador donde las modificaciones al HashMap hechas por un hilo (por ejemplo, el que lo rellenó) pueden permanecer en el caché de esa CPU y no serán vistas por hilos que se ejecutan en otras CPU hasta que se realice una operación de valla de memoria realizado asegurando la coherencia del caché. La especificación del lenguaje Java es explícita en este caso: la solución es adquirir un locking (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 locking, entonces está bien desde ese momento para acceder al HashMap desde cualquier hilo hasta que HashMap se modifique nuevamente.

De acuerdo con http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Initialization Safety puede hacer de su HashMap un campo final y una vez que el constructor finalice, se publicará con seguridad.

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

Entonces, el escenario que describes es que necesitas poner un montón de datos en un Mapa, y cuando termines de poblarlo lo tratas como inmutable. Un enfoque que es “seguro” (lo que significa que usted está haciendo cumplir que realmente se trata como inmutable) es reemplazar la referencia con Collections.unmodifiableMap (originalMap) cuando esté listo para hacerlo inmutable.

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

Tenga en cuenta que incluso en el código de un solo subproceso, reemplazar un ConcurrentHashMap con un HashMap puede no ser seguro. ConcurrentHashMap prohíbe nulo como clave o valor. HashMap no los prohíbe (no preguntes).

Por lo tanto, en la situación poco probable de que su código existente agregue un valor nulo a la colección durante la configuración (presumiblemente en un caso de falla de algún tipo), reemplazar la colección como se describe cambiará el comportamiento funcional.

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

[Editar: por “lecturas concurrentes”, quiero decir que no hay 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 que iniciar un hilo sea una acción sincronizada, lo que significa que los cambios realizados en el hilo A antes de que comience el hilo B son visibles en el hilo B.

Mi intención no es contradecir las respuestas más detalladas sobre el Modelo de memoria de Java. Esta respuesta pretende señalar que, aparte de los problemas de simultaneidad, existe al menos una diferencia de API entre ConcurrentHashMap y HashMap, que podría anular incluso un progtwig de subproceso único que reemplazó uno con el otro.

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 locking / mutex allí.

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

Lo que es interesante es que tanto .NET HashTable como Dictionary tienen incorporado el código de sincronización.

Si la inicialización y cada entrada están sincronizados, está guardado.

El código siguiente se guarda porque el cargador de clases se ocupará de la sincronización:

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

El código siguiente se guarda porque la escritura de volátil se ocupará de la sincronización.

 class Foo { volatile HashMap map; public void init() { final HashMap 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 definitiva porque final también es volátil. Y si el método es un constructor.