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.HashMap
de 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 HashMap
serí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?")
java
multithreading
concurrency
hashmap
Dave L.
fuente
fuente
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.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.Respuestas:
Su idioma está a salvo si y solo si la referencia al
HashMap
se publica de forma segura . En lugar de cualquier cosa relacionada con lo interno deHashMap
sí 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
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 del mapa, pero esto es irrelevante ya que nunca lo modifica, por lo que la única parte interesante es cómoHashMap
se publica la referencia.Por ejemplo, imagina que publicas el mapa así:
... y en algún momento
setMap()
se llama con un mapa, y otros subprocesos se utilizanSomeClass.MAP
para acceder al mapa y verificar nulos como este: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.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 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] :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:entonces todo es kosher: los lectores que ven un valor no nulo necesariamente tienen una relación de " pasa antes" con la tienda
MAP
y, 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
MAP
dinámicamente en tiempo de ejecución. Si no necesita hacer eso, simplemente declareMAP
como astatic 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:final
campo (inclusostatic final
para miembros estáticos).¡Eso es!
En la práctica, es muy eficiente. El uso de un
static final
campo, 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 unfinal
campo 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
volatile
sí 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 seguraHashMap
, puedes almacenar tantosHashMap
correos 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
synchronized
método para obtener / configurar, o unAtomicReference
o 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
final
lectura - pero estos son muy raros hoy en día.fuente
never modify
HashMap
No significa que elstate of the map object
hilo es seguro, creo. Dios conoce la implementación de la biblioteca, si el documento oficial no dice que es seguro para subprocesos.get()
podría, de hecho, realizar algunas escrituras, por ejemplo, actualizar algunas estadísticas (o en el caso de una orden ordenada de acceso queLinkedHashMap
actualiza la orden de acceso). Por lo tanto, una clase bien escrita debería proporcionar documentación que aclare si ...const
es verdaderamente de solo lectura en ese sentido (internamente, aún pueden realizar escrituras, pero deberán hacerse seguras para subprocesos). No existe unaconst
palabra 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 elLinkedHashMap
ejemplo donde las operaciones de RO comoget
se mencionan explícitamente como inseguras).HashMap
que 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.)HashMap
métodos que esperamos sean de solo lectura, son de solo lectura, ya que no modifican estructuralmente elHashMap
. Por supuesto, esta garantía podría no ser válida para otrasMap
implementaciones arbitrarias , pero la pregunta esHashMap
específicamente.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
fuente
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.
fuente
Después de mirar un poco más, encontré esto en el documento de Java (énfasis mío):
Esto parece implicar que será seguro, suponiendo que lo contrario de la afirmación sea cierto.
fuente
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
fuente
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.
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.
fuente
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. ...
fuente
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
fuente
Esta pregunta se aborda en el libro "Concurrencia de Java en la práctica" de Brian Goetz (Listado 16.8, página 350):
Dado que
states
se declara comofinal
y 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.fuente
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.]
fuente
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.
fuente
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:
El siguiente código se guarda porque la escritura de volátil se encargará de la sincronización.
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.
fuente