safe - Java 8 ConcurrentHashMap
java hashmap thread safe (3)
Respuesta corta
El Node#val
es volatile
que establece su suceso antes de ordenar.
Respuesta más larga
synchronized
no es un requisito para la seguridad de subprocesos, es una herramienta en una caja de herramientas para hacer que un subproceso del sistema sea seguro. Tendrá que considerar un conjunto completo de acciones en este ConcurrentHashMap
de ConcurrentHashMap
para razonar sobre la seguridad del hilo.
Es útil saber que el ConcurrentHashMap
original también es no bloqueante. Aviso pre-Java 8 CHM obtener
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // ignore this
}
e = e.next;
}
}
return null;
}
En este caso, no hay bloqueo, entonces, ¿cómo funciona? El HashEntry#value
es volatile
. Ese es el punto de sincronización para la seguridad del hilo.
La clase Node
para CHM-8 es la misma.
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Por lo tanto, un valor que no sea nulo en este caso debe garantizar que suceda antes de la relación con respecto a las acciones antes de una venta.
He observado que ConcurrentHashMap se ha reescrito completamente en Java 8 para que sea más "sin bloqueo". He explorado el código del método get()
y veo que no hay un mecanismo de bloqueo explícito:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
Pregunta:
¿Cómo es posible ver desde un subproceso, las modificaciones realizadas a este hashmap desde otros subprocesos, ya que el código no está bajo un paraguas de sincronización (que obligaría a una relación de suceso antes )?
Nota: todo el ConcurrentHashMap es una envoltura de una tabla: transient volatile Node<K,V>[] table;
¡Así que la table
es una referencia volátil a una matriz, no una referencia a una matriz de elementos volátiles! Lo que significa que si alguien está actualizando un elemento dentro de esta matriz, la modificación no se verá en otros subprocesos.
La documentación no indica que se produzca la sincronización. Por ejemplo dice
[...] operaciones agregadas como
putAll
yclear
, las recuperaciones simultáneas pueden reflejar la inserción o eliminación de solo algunas entradas.
En otras palabras, hay una diferencia entre permitir el uso concurrente y proporcionar acceso sincronizado.
La especificación del lenguaje Java writes :
Si tenemos dos acciones x e y, escribimos hb (x, y) para indicar que x sucede antes de y.
Si x e y son acciones del mismo hilo y x aparece antes de y en el orden del programa, entonces hb (x, y).
Hay un borde antes del final de un constructor de un objeto hasta el inicio de un finalizador (§12.6) para ese objeto.
Si una acción x se sincroniza con una acción siguiente y, entonces también tenemos hb (x, y).
Si hb (x, y) y hb (y, z), entonces hb (x, z).
y defines
Las acciones de sincronización inducen la relación sincronizada con las acciones, definida de la siguiente manera:
Una acción de desbloqueo en el monitor m se sincroniza con todas las acciones de bloqueo subsiguientes en m (donde "subsiguiente" se define de acuerdo con el orden de sincronización).
Una escritura en una variable volátil v (§8.3.1.4) se sincroniza con todas las lecturas subsiguientes de v por cualquier hilo (donde "subsiguiente" se define de acuerdo con el orden de sincronización).
Una acción que inicia un hilo se sincroniza con la primera acción en el hilo que comienza.
La escritura del valor predeterminado (cero, falso o nulo) en cada variable se sincroniza con la primera acción en cada subproceso.
Aunque puede parecer un poco extraño escribir un valor predeterminado en una variable antes de que se asigne el objeto que contiene la variable, conceptualmente, cada objeto se crea al inicio del programa con sus valores inicializados predeterminados.
La acción final en un hilo T1 se sincroniza con cualquier acción en otro hilo T2 que detecta que T1 ha terminado.
T2 puede lograr esto llamando a T1.isAlive () o T1.join ().
Si el subproceso T1 interrumpe el subproceso T2, la interrupción por T1 se sincroniza con cualquier punto donde cualquier otro subproceso (incluido T2) determina que T2 se ha interrumpido (al tener una excepción interrumpida o invocando Thread.interrupted o Thread.isInterrupted).
Es decir, la lectura de un campo volátil establece el suceso antes como un bloqueo explícito.