Pregunta ¿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 este modo?

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


110
2017-09-19 18:11


origen


Respuestas:


Tu idioma es seguro si y solo si la referencia al HashMap es publicado con seguridad. En lugar de nada relacionado con el funcionamiento interno de HashMap sí mismo, publicación segura 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 de la HashMap y cualquier hilo de lectura que pueda acceder antes de que esté completamente construido. La mayor parte de la discusión trata de lo que sucede con el estado del objeto del mapa, pero esto es irrelevante ya que nunca lo modifica, así que la única parte interesante es cómo el HashMap referencia es publicada.

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 verifica nulo así:

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

Esto es no es seguro aunque probablemente parezca como si lo fuera. El problema es que no hay pasa-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 de 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 ciclo infinito.

Para publicar el mapa de manera segura, necesita establecer un pasa-antes relación entre el escritura de la referencia al HashMap (es decir, el publicación) y los lectores posteriores de esa referencia (es decir, el consumo). Convenientemente, solo hay algunas maneras fáciles de recordar para realizar ese[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. 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. 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 declaración de MAP a:

public static volatile HashMap<Object, Object> MAP;

entonces todo es kosher: lectores que ven una no nulo valor necesariamente tener una pasa-antes relación 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 initalizador estático) como (4) (usando final) implica que no puedes establecer MAP dinámicamente en tiempo de ejecución. Si no lo haces necesitar para hacer eso, entonces solo declara 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 inherentemente inmutables (como en todos los campos declarados final) y:

  • Ya puedes crear el objeto que se asignará en el momento de la declaraciónun: solo usa un final campo (incluido static final para miembros estáticos).
  • Desea asignar el objeto más tarde, después de que la referencia ya esté visible: use un campo volátilsegundo.

¡Eso es!

En la práctica, es muy eficiente. El uso de un static final campo, por ejemplo, permite a la JVM asumir que el valor no se modifica durante la vida del programa y optimizarlo en gran medida. El uso de un final campo miembro permite más arquitecturas para leer el campo de una manera equivalente a una lectura de campo normal y no inhibe otras optimizacionesdo.

Finalmente, el uso de volatile tiene algún impacto: no se necesita ninguna barrera de hardware en muchas arquitecturas (como x86, específicamente aquellas que no permiten que las lecturas pasen lecturas), pero algunas optimizaciones y reordenamientos pueden no ocurrir en tiempo de compilación, pero este efecto es generalmente pequeño. A cambio, obtienes más de lo que pedías, no solo puedes publicar uno HashMap, puedes almacenar tantos más no modificados HashMaps como quiera a la misma referencia y tenga la seguridad de que todos los lectores verán un mapa publicado con seguridad.

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


[1] Citando directamente de Shipilev.


un 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 declaración o en el constructor (campos de miembros) o el inicializador estático (campos estáticos).

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

c Algunas arquitecturas con modelos de memoria muy débiles (estoy mirando , Alpha) puede requerir algún tipo de barrera de lectura antes de una final leer, pero estos son muy raros hoy.


27
2018-02-01 21:45



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 de Inmutabilidad en Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html 

Parte 2 de 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


70
2017-09-19 18:56



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 para 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.

Ver Artículo de Brian Goetz sobre el nuevo modelo de memoria de Java para detalles.


34
2017-09-19 19:10



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

Tenga en cuenta que esta implementación no es   sincronizado. Si múltiples hilos   acceder a un mapa hash al mismo tiempo, y en   al menos uno de los hilos modifica el   mapa estructuralmente, debe ser   sincronizado externamente (Un estructural   modificación es cualquier operación que   agrega o elimina una o más asignaciones;   simplemente cambiando el valor asociado   con una clave que una instancia ya   contiene no es estructural   modificación.)

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


9
2017-09-19 18:20



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 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 desde ese momento para acceder al HashMap desde cualquier hilo hasta que HashMap se modifique nuevamente.


8
2017-09-19 19:16



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


8
2017-09-19 21:13



De acuerdo a http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Seguridad de inicialización puede hacer que su HashMap sea 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. ...


4
2017-08-30 16:08



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 lo mal que pueden fallar los mapas si se usan simultáneamente, y la solución sugerida que mencioné, revisa esta entrada del desfile de fallos: bug_id = 6423457


3
2017-11-09 16:37