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 de HashMap
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ómo HashMap
se 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.MAP
para 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.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] :
- Intercambie la referencia a través de un campo bloqueado correctamente ( JLS 17.4.5 )
- Use el inicializador estático para hacer las tiendas de inicialización ( JLS 12.4 )
- 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
- 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: 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 declare MAP
como 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
final
campo (incluso 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átil b .
¡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 un final
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 segura HashMap
, puedes almacenar tantos HashMap
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 un AtomicReference
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.