español - Comportamiento de la barrera de la memoria en Java.
garbage collector java example (2)
Después de leer más blogs / artículos, etc., ahora estoy realmente confundido sobre el comportamiento de carga / almacenamiento antes / después de la barrera de la memoria.
A continuación hay 2 citas de Doug Lea en uno de sus artículos de aclaración sobre JMM, que son muy directos:
- Todo lo que era visible para el hilo A cuando escribe en el campo f volátil se vuelve visible para el hilo B cuando lee f.
- Tenga en cuenta que es importante que ambos subprocesos accedan a la misma variable volátil para configurar correctamente la relación de suceso antes. No es el caso que todo lo visible para el hilo A cuando escribe el campo f volátil se vuelve visible para el hilo B después de leer el campo volátil g.
Pero luego, cuando miré en otro blog sobre la barrera de la memoria, obtuve estos:
- Una barrera de almacenamiento, "instrucción de referencia" en x86, obliga a que todas las instrucciones de almacenamiento anteriores a la barrera ocurran antes de la barrera y hacen que los almacenamientos intermedios del almacén se vacíen en la memoria caché de la CPU en la que se emitió.
- Una barrera de carga, la instrucción “lfence” en x86, obliga a que todas las instrucciones de carga después de la barrera se realicen después de la barrera y luego esperen a que el buffer de carga se vacíe para esa CPU.
Para mí, la explicación de Doug Lea es más estricta que la otra: básicamente, significa que si la barrera de carga y la barrera de almacenamiento están en diferentes monitores, no se garantizará la consistencia de los datos. Pero el último significa que incluso si las barreras están en diferentes monitores, se garantizará la consistencia de los datos. No estoy seguro de si entendí estos 2 correctamente y tampoco estoy seguro de cuál de ellos es correcto.
Teniendo en cuenta los siguientes códigos:
public class MemoryBarrier {
volatile int i = 1, j = 2;
int x;
public void write() {
x = 14; //W01
i = 3; //W02
}
public void read1() {
if (i == 3) { //R11
if (x == 14) //R12
System.out.println("Foo");
else
System.out.println("Bar");
}
}
public void read2() {
if (j == 2) { //R21
if (x == 14) //R22
System.out.println("Foo");
else
System.out.println("Bar");
}
}
}
Digamos que tenemos 1 subproceso de escritura TW1 primero llama al método write () de MemoryBarrier, luego tenemos 2 subprocesos de lectura TR1 y TR2 llamado método read1 () y read2 () de MemoryBarrier. Considera que este programa se ejecute en la CPU que no conserva el ordenamiento (x86 DEBE conservar el orden para tales casos, que no es el caso), según el modelo de memoria, habrá una barrera StoreStore (digamos SB1) entre W01 / W02, así como 2 barreras LoadLoad entre R11 / R12 y R21 / R22 (vamos a decir RB1 y RB2).
- Dado que SB1 y RB1 están en el mismo monitor i , entonces el subproceso TR1 que llama a read1 siempre debería ver 14 en x, también se imprime "Foo".
- SB1 y RB2 están en diferentes monitores, si Doug Lea está en lo correcto, no se garantiza que la rosca TR2 vea 14 en x, lo que significa que "Bar" puede imprimirse ocasionalmente. Pero si la barrera de memoria se ejecuta como lo describe Martin Thompson en el blog , la barrera de la Tienda enviará todos los datos a la memoria principal y la barrera de Carga extraerá todos los datos de la memoria principal a la caché / búfer, entonces se garantizará que TR2 vea 14 en x.
No estoy seguro de cuál es el correcto, o ambos lo son, pero lo que Martin Thompson describió es solo para la arquitectura x86. JMM no garantiza que el cambio a x sea visible para TR2, pero la implementación de x86 sí lo hace.
Gracias ~
Doug Lea tiene razón. Puede encontrar la parte relevante en la sección §17.4.4 de la Especificación del lenguaje Java :
[..] 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 según el orden de sincronización). [..]
El modelo de memoria de la máquina concreta no importa, porque la semántica del lenguaje de programación Java se define en términos de una máquina abstracta , independiente de la máquina concreta . Es responsabilidad del entorno de ejecución de Java ejecutar el código de tal manera que cumpla con las garantías dadas por la especificación del lenguaje Java .
Respecto a la pregunta actual:
- Si no hay más sincronización, el método
read2
puede imprimir"Bar"
, porqueread2
puede ejecutarse antes dewrite
. - Si hay una sincronización adicional con
CountDownLatch
para asegurarse de queread2
se ejecuta después de lawrite
, el métodoread2
nunca imprimirá"Bar"
, porque la sincronización conCountDownLatch
elimina la carrera de datos enx
.
Variables volátiles independientes:
¿Tiene sentido que una escritura en una variable volátil no se sincronice con una lectura de otra variable volátil?
Sí, tiene sentido. Si dos hilos necesitan interactuar entre sí, generalmente tienen que usar la misma variable volatile
para intercambiar información. Por otro lado, si un subproceso utiliza una variable volátil sin la necesidad de interactuar con todos los demás subprocesos, no queremos pagar el costo de una barrera de memoria.
En realidad es importante en la práctica. Hagamos un ejemplo. La siguiente clase usa una variable miembro volátil:
class Int {
public volatile int value;
public Int(int value) { this.value = value; }
}
Imagina que esta clase se usa solo localmente dentro de un método. El compilador JIT puede detectar fácilmente que el objeto solo se usa dentro de este método ( análisis de escape ).
public int deepThought() {
return new Int(42).value;
}
Con la regla anterior, el compilador JIT puede eliminar todos los efectos de las lecturas y escrituras volatile
, ya que la variable volatile
no puede ser accesada desde ningún otro hilo.
Esta optimización realmente existe en el compilador JIT de Java:
Hasta donde entiendo, la pregunta es en realidad acerca de las lecturas / escrituras volátiles y sus garantías antes de que ocurran. Hablando de esa parte, solo tengo una cosa que añadir a la respuesta de nosid:
Las escrituras volátiles no se pueden mover antes de las escrituras normales, las lecturas volátiles no se pueden mover después de las lecturas normales. Es por eso que los resultados de read1()
y read2()
serán como nosid escribió.
Hablando de barreras, la definición me suena bien, pero lo que probablemente te confundió es que estas son cosas / herramientas / camino a / mecanismo (llámalo como quieras) para implementar el comportamiento descrito en JMM en el hotspot. Al utilizar Java, debe confiar en las garantías de JMM, no en los detalles de implementación.