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

138

Hay un caso en el que se construirá un mapa, y una vez que se inicialice, nunca más se modificará. Sin embargo, se accederá (solo mediante get (clave)) desde varios subprocesos. ¿Es seguro usar un java.util.HashMapde esta manera?

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

Dave L.
fuente
44
Muchas respuestas aquí son correctas con respecto a la exclusión mutua de subprocesos en ejecución, pero incorrecta con respecto a las actualizaciones de memoria. He votado arriba / abajo en consecuencia, pero todavía hay muchas respuestas incorrectas con votos positivos.
Heath Borders
@Heath Borders, si la instancia a fue HashMap no modificable estáticamente inicializada, debería ser segura para la lectura concurrente (ya que otros hilos no podrían haber perdido actualizaciones ya que no hubo actualizaciones), ¿verdad?
kaqqao
Si se inicializa estáticamente y nunca se modifica fuera del bloque estático, entonces podría estar bien porque toda la inicialización estática está sincronizada por el ClassLoader. Eso vale una pregunta separada por sí solo. Todavía lo sincronizaría explícitamente y el perfil para verificar que estaba causando problemas de rendimiento reales.
Heath Borders
@HeathBorders: ¿qué quiere decir con "actualizaciones de memoria"? El JVM es un modelo formal que define cosas como la visibilidad, la atomicidad, las relaciones antes de pasar , pero no utiliza términos como "actualizaciones de memoria". Debe aclarar, preferiblemente utilizando la terminología del JLS.
BeeOnRope
2
@Dave: supongo que aún no está buscando respuesta después de 8 años, pero para el registro, la confusión clave en casi todas las respuestas es que se centran en las acciones que realiza en el objeto del mapa . Ya ha explicado que nunca modifica el objeto, por lo que todo eso es irrelevante. El único "problema" potencial es cómo publicas la referencia al Map, que no explicaste. Si no lo hace de manera segura, no es seguro. Si lo haces con seguridad, lo es . Detalles en mi respuesta.
BeeOnRope

Respuestas:

55

Su idioma está a salvo si y solo si la referencia al HashMapse publica de forma segura . En lugar de cualquier cosa relacionada con lo interno de HashMapsí mismo, la 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 del HashMapy 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 del mapa, pero esto es irrelevante ya que nunca lo modifica, por lo que la única parte interesante es cómo HashMapse publica la referencia.

Por ejemplo, imagina que publicas el mapa así:

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 subprocesos se utilizan SomeClass.MAPpara acceder al mapa y verificar nulos como este:

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

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

Para publicar el mapa de forma segura, debe establecer una relación de suceso previo entre la redacción 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 de 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. 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 MAPa:

public static volatile HashMap<Object, Object> MAP;

entonces todo es kosher: los lectores que ven un valor no nulo necesariamente tienen una relación de " pasa antes" con la tienda MAPy, por lo tanto, ven 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 inicializador estático) como (4) (usando final ) implican que no puede establecer MAPdinámicamente en tiempo de ejecución. Si no necesita hacer eso, simplemente declare MAPcomo a 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 inmutable inherentemente (como en todos los campos declarados final) y:

  • Ya puede crear el objeto que se asignará en el momento de la declaración a : solo use un finalcampo (incluso static finalpara 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 static finalcampo, por ejemplo, permite que la JVM asuma que el valor no cambia durante la vida del programa y lo optimiza en gran medida. El uso de un finalcampo miembro permite que la mayoría de las arquitecturas lean el campo de una manera equivalente a una lectura de campo normal y no inhiben optimizaciones adicionales c .

Finalmente, el uso de volatilesí tiene algún impacto: no se necesita barrera de hardware en muchas arquitecturas (como x86, específicamente aquellas que no permiten que las lecturas pasen las lecturas), pero puede que no se produzca alguna optimización y reordenamiento en el momento de la compilación, pero esto El efecto es generalmente pequeño. A cambio, obtienes más de lo que pediste: no solo puedes publicar uno de forma segura HashMap, puedes almacenar tantos HashMapcorreos electrónicos no modificados como quieras para la misma referencia y asegurarte de que todos los lectores verán un mapa publicado de forma segura .

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


[1] Citando directamente de shipilev .


a Eso suena complicado, pero lo que quiero decir es que puede asignar la referencia en el momento de la 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 synchronizedmétodo para obtener / configurar, o un AtomicReferenceo algo, pero estamos hablando del trabajo mínimo que puede hacer.

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

BeeOnRope
fuente
never modify HashMapNo significa que el state of the map objecthilo es seguro, creo. Dios conoce la implementación de la biblioteca, si el documento oficial no dice que es seguro para subprocesos.
Jiang YD
@JiangYD: tiene razón, hay un área gris allí en algunos casos: cuando decimos "modificar", lo que realmente queremos decir es cualquier acción que realice internamente algunas escrituras que puedan competir con lecturas o escrituras en otros hilos. Estas escrituras pueden ser detalles de implementación internos, por lo que incluso una operación que parece "solo lectura" get()podría, de hecho, realizar algunas escrituras, por ejemplo, actualizar algunas estadísticas (o en el caso de una orden ordenada de acceso que LinkedHashMapactualiza la orden de acceso). Por lo tanto, una clase bien escrita debería proporcionar documentación que aclare si ...
BeeOnRope
... aparentemente las operaciones de "solo lectura" son internamente de solo lectura en el sentido de seguridad de subprocesos. En la biblioteca estándar de C ++, por ejemplo, hay una regla general de que la función miembro marcada constes verdaderamente de solo lectura en ese sentido (internamente, aún pueden realizar escrituras, pero deberán hacerse seguras para subprocesos). No existe una constpalabra clave en Java y no conozco ninguna garantía general documentada, pero en general las clases de biblioteca estándar se comportan como se esperaba, y las excepciones están documentadas (vea el LinkedHashMapejemplo donde las operaciones de RO como getse mencionan explícitamente como inseguras).
BeeOnRope
@JiangYD: finalmente, volviendo a su pregunta original, ya HashMapque en realidad tenemos en la documentación el comportamiento de seguridad de subprocesos para esta clase: si varios subprocesos acceden a un mapa hash al mismo tiempo, y al menos uno de los subprocesos modifica el mapa estructuralmente, 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 ya contiene una instancia no es una modificación estructural.)
BeeOnRope
Entonces, para los HashMapmétodos que esperamos sean de solo lectura, son de solo lectura, ya que no modifican estructuralmente el HashMap. Por supuesto, esta garantía podría no ser válida para otras Mapimplementaciones arbitrarias , pero la pregunta es HashMapespecíficamente.
BeeOnRope
70

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

Para obtener más información sobre el tema, lea las publicaciones del 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

Taylor Gautier
fuente
3
Es un buen punto, pero confío en la inicialización estática, durante la cual no se escapan las referencias, por lo que debería ser seguro.
Dave L.
55
No veo cómo esta es una respuesta altamente calificada (o incluso una respuesta). No responde , por ejemplo, a la pregunta y no menciona el principio clave que decidirá si es seguro o no: la publicación segura . La "respuesta" se reduce a "es engañoso" y aquí hay tres enlaces (complejos) que puede leer.
BeeOnRope
Él responde la pregunta al final de la primera oración. En términos de ser una respuesta, está planteando el punto de que la inmutabilidad (aludida en el primer párrafo de la pregunta) no es sencilla, junto con recursos valiosos que explican ese tema aún más. Los puntos no miden si es una respuesta, mide si la respuesta fue "útil" para otros. La respuesta aceptada significa que fue la respuesta que estaba buscando el OP, que recibió su respuesta.
Jesse
@Jesse él no responde la pregunta al final de la primera oración, responde la pregunta "¿es seguro acceder a un objeto inmutable", que puede o no aplicarse a la pregunta del OP como señala en la siguiente oración. Esencialmente, esta es una respuesta de tipo "solo descúbrelo tú mismo", casi un enlace, que no es una buena respuesta para SO. En cuanto a los votos a favor, creo que es más una función de tener 10.5 años y un tema buscado con frecuencia. Ha recibido solo unos pocos votos a favor netos en los últimos años, por lo que tal vez la gente se acerca :).
BeeOnRope
35

Las lecturas están seguras desde el punto de vista de sincronización pero no desde el punto de vista de la memoria. Esto es algo que los desarrolladores de Java no entienden ampliamente, 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 en memoria se producen mediante el uso de palabras clave sincronizadas o volátiles, o mediante el uso de algunas construcciones de concurrencia de Java.

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

Heath Borders
fuente
Perdón por el envío doble Heath, solo noté el tuyo después de enviar el mío. :)
Alexander
2
Me alegra que haya otras personas aquí que realmente entiendan los efectos de la memoria.
Heath Borders
1
De hecho, aunque ningún hilo verá el objeto antes de que se inicialice correctamente, por lo que no creo que sea una preocupación en este caso.
Dave L.
1
Eso depende completamente de cómo se inicializa el objeto.
Bill Michell
1
La pregunta dice que una vez que se ha inicializado el HashMap, no tiene intención de actualizarlo más. A partir de entonces, solo quiere usarlo como una estructura de datos de solo lectura. Creo que sería seguro hacerlo, siempre que los datos almacenados en su Mapa sean inmutables.
Binita Bharati
9

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

Tenga en cuenta que esta implementación no está sincronizada. Si varios subprocesos acceden a un mapa hash al mismo tiempo, y al menos uno de los subprocesos modifica el mapa estructuralmente, debe sincronizarse 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 ya contiene una instancia no es una modificación estructural).

Esto parece implicar que será seguro, suponiendo que lo contrario de la afirmación sea cierto.

Dave L.
fuente
1
Si bien este es un excelente consejo, como dicen otras respuestas, hay una respuesta más matizada en el caso de una instancia de mapa inmutable y publicada de manera segura. Pero debe hacerlo solo si sabe lo que está haciendo.
Alex Miller
1
Esperemos que con preguntas como estas, más de nosotros podamos saber lo que estamos haciendo.
Dave L.
Esto no es realmente correcto. Como dicen las otras respuestas, debe haber un evento previo entre la última modificación y todas las lecturas posteriores "seguras para subprocesos". Normalmente, esto significa que debe publicar el objeto de forma segura después de que se haya creado y se hayan realizado sus modificaciones. Vea la primera respuesta correcta marcada.
markspace
9

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 provoca una repetición del Mapa.

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

Alex Miller
fuente
1
De hecho, he visto este colgar la JVM sin consumir CPU (que es tal vez peor)
Peter Lawrey
2
Creo que este código ha sido reescrito de tal manera que ya no es posible obtener el bucle infinito. Pero aún no debería estar presionando para obtener y poner desde un HashMap no sincronizado por otras razones.
Alex Miller
@AlexMiller, incluso aparte de las otras razones (supongo que se refiere a una publicación segura), no creo que un cambio de implementación deba ser una razón para aflojar las restricciones de acceso, a menos que la documentación lo permita explícitamente. Como sucede, el HashMap Javadoc para Java 8 todavía contiene esta advertencia:Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
shmosel
8

Sin embargo, hay un giro importante. Es seguro acceder al mapa, pero en general no está garantizado que todos los hilos verán exactamente el mismo estado (y, por lo tanto, los valores) de HashMap. Esto puede suceder en sistemas multiprocesador donde las modificaciones al HashMap realizadas por un subproceso (por ejemplo, el que lo pobló) pueden ubicarse en el caché de esa CPU y no serán vistas por los subprocesos que se ejecutan en otras CPU, hasta que se realice una operación de valla de memoria realizado asegurando la coherencia de 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. Entonces, si está seguro de que después de llenar el HashMap, cada uno de los subprocesos adquiere CUALQUIER bloqueo, a partir de ese momento está bien acceder al HashMap desde cualquier subproceso hasta que el HashMap se modifique nuevamente.

Alejandro
fuente
No estoy seguro de que el hilo que accede a él adquiera algún bloqueo, pero estoy seguro de que no obtendrán una referencia al objeto hasta después de que se haya inicializado, por lo que no creo que puedan tener una copia obsoleta.
Dave L.
@Alex: la referencia al HashMap puede ser volátil para crear las mismas garantías de visibilidad de memoria @Dave: es posible ver referencias a nuevos objs antes de que el trabajo de su ctor sea visible para su hilo.
Chris Vest
@Christian En el caso general, ciertamente. Estaba diciendo que en este código, no lo es.
Dave L.
La adquisición de un bloqueo ALEATORIO no garantiza que se borre todo el caché de la CPU del hilo. Depende de la implementación de JVM, y es muy probable que no se haga de esta manera.
Pierre
Estoy de acuerdo con Pierre, no creo que adquirir un candado sea suficiente. Debe sincronizar en el mismo candado para que los cambios sean visibles.
Damluar
5

De acuerdo con 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 que ocurre 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. ...

bodrin
fuente
Esta respuesta es de baja calidad, es la misma que la respuesta de @taylor gauthier pero con menos detalles.
Snicolas
1
Ummmm ... no ser un idiota, pero lo tienes al revés. Taylor dijo "no, ve a esta publicación de blog, la respuesta podría sorprenderte", mientras que esta respuesta en realidad agrega algo nuevo que no sabía ... Sobre una relación que ocurre antes de la escritura de un campo final en un constructor. Esta respuesta es excelente, y me alegro de haberla leído.
Ajax
¿Eh? Esta es la única respuesta correcta que encontré después de desplazarme por las respuestas mejor calificadas. La clave se publica de forma segura y esta es la única respuesta que incluso lo menciona.
BeeOnRope
3

Entonces, el escenario que describió es que necesita poner un montón de datos en un Mapa, luego, cuando termine de poblarlo, lo tratará como inmutable. Un enfoque que es "seguro" (lo que significa que 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é, consulte esta entrada del desfile de errores: bug_id = 6423457

Será
fuente
2
Esto es "seguro" ya que impone la inmutabilidad, pero no aborda el problema de seguridad de subprocesos. Si el mapa es seguro para acceder con el contenedor UnmodifiableMap, entonces es seguro sin él, y viceversa.
Dave L.
2

Esta pregunta se aborda en el libro "Concurrencia de Java en la práctica" de Brian Goetz (Listado 16.8, página 350):

@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
    }

    public String getAbbreviation(String s) {
        return states.get(s);
    }
}

Dado que statesse declara como finaly su inicialización se realiza dentro del constructor de la clase del propietario, se garantiza que cualquier subproceso que luego lea este mapa lo verá en el momento en que finalice el constructor, siempre que ningún otro subproceso intente modificar el contenido del mapa.

escudero380
fuente
1

Tenga en cuenta que incluso en un código de subproceso único, reemplazar un ConcurrentHashMap por un HashMap puede no ser seguro. ConcurrentHashMap prohíbe nulo como clave o valor. HashMap no los prohíbe (no pregunte).

Por lo tanto, en la situación poco probable de que su código existente pueda agregar 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 garantizar esto. Una forma es hacer que el mapa sea inmutable, pero no es necesario. Por ejemplo, el modelo de memoria JSR133 define explícitamente el inicio de un subproceso como una acción sincronizada, lo que significa que los cambios realizados en el subproceso A antes de que comience el subproceso B son visibles en el subproceso B.

Mi intención no es contradecir esas respuestas más detalladas sobre el modelo de memoria Java. Esta respuesta tiene como objetivo señalar que, incluso aparte de los problemas de concurrencia, hay al menos una diferencia de API entre ConcurrentHashMap y HashMap, que podría arruinar incluso un programa de subproceso único que reemplazó uno con el otro.]

Steve Jessop
fuente
Gracias por la advertencia, pero no hay intentos de usar claves o valores nulos.
Dave L.
Pensé que no habría. los nulos en las colecciones son un loco rincón de Java.
Steve Jessop
No estoy de acuerdo con esta respuesta. "Las lecturas concurrentes de un HashMap son seguras" en sí mismas son incorrectas. No indica si las lecturas se producen en un mapa que es mutable o inmutable. Para ser correcto, debería leer "Las lecturas concurrentes de un HashMap inmutable son seguras"
Taylor Gautier
2
No de acuerdo con los artículos a los que se vinculó: el requisito es que el mapa no se debe cambiar (y los cambios anteriores deben ser visibles para todos los hilos del lector), no que sea inmutable (que es un término técnico en Java y es un condición suficiente pero no necesaria para la seguridad).
Steve Jessop
También una nota ... la inicialización de una clase se sincroniza implícitamente en el mismo bloqueo (sí, puede bloquearse en los inicializadores de campo estático), por lo que si su inicialización se realiza de forma estática, sería imposible que alguien más la vea antes de que se complete la inicialización, como tendrían que ser bloqueados en el método ClassLoader.loadClass en el mismo bloqueo adquirido ... Y si se pregunta si diferentes cargadores de clases tienen diferentes copias del mismo campo, sería correcto ... pero eso sería ortogonal al noción de las condiciones de carrera; Los campos estáticos de un cargador de clases comparten una cerca de memoria.
Ajax
0

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

Aquí está la fuente de 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 múltiples escrituras.

Lo interesante es que tanto .NET HashTable como Dictionary <K, V> han incorporado un código de sincronización.

FlySwat
fuente
2
Creo que hay clases en las que simplemente leer simultáneamente puede meterte en problemas, debido al uso interno de variables de instancia temporales, por ejemplo. Por lo tanto, probablemente sea necesario examinar cuidadosamente la fuente, más que un escaneo rápido para el código de bloqueo / mutex.
Dave L.
0

Si la inicialización y cada colocación están sincronizadas, está guardado.

El siguiente código se guarda porque el cargador de clases 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 volátil 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.

TomWolk
fuente