java - definicion - variable volatile
Java: garantías implícitas volátiles de orden. (2)
Mi pregunta es una extensión a esta: garantías volátiles y ejecución fuera de orden
Para hacerlo más concreto, digamos que tenemos una clase simple que puede estar en dos estados después de su inicialización:
class A {
private /*volatile?*/ boolean state;
private volatile boolean initialized = false;
boolean getState(){
if (!initialized){
throw new IllegalStateException();
}
return state;
}
void setState(boolean newState){
state = newState;
initialized = true;
}
}
El campo inicializado se declara volátil , por lo que introduce suceso antes de "barrera" que garantiza que no se pueda reordenar. Dado que el campo de estado se escribe solo antes de que el campo inicializado se escriba y se lea solo después de que se lea el campo inicializado , puedo eliminar la palabra clave volátil de la declaración del estado y aún nunca ver un valor obsoleto. Las preguntas son:
- ¿Es correcto este razonamiento?
- ¿Se garantiza que la escritura en el campo inicializado no se optimizará (ya que cambia solo la primera vez) y que la "barrera" no se perderá?
Supongamos que, en lugar de la bandera, se usó CountDownLatch como inicializador como este:
class A { private /*volatile?*/ boolean state; private final CountDownLatch initialized = new CountDownLatch(1); boolean getState() throws InterruptedException { initialized.await(); return state; } void setState(boolean newState){ state = newState; initialized.countdown(); } }
¿Todavía estaría bien?
1. ¿Es correcto este razonamiento?
No, el estado se almacenará en caché en el subproceso, por lo que no puede obtener el último valor.
2. ¿Se garantiza que el campo de escritura en inicializado no se optimizará (ya que cambia solo la primera vez) y la "barrera" no se perderá?
Sí
3. Supongamos que, en lugar de la bandera, se usó CountDownLatch como inicializador como este ...
al igual que @ratchet freak, CountDownLatch es un bloqueo de tiempo, mientras que volatile es un tipo de cierre reutilizable, por lo que la respuesta para su tercera pregunta debería ser: si va a establecer el estado varias veces, debe usar volatile .
Su código es (en su mayoría) correcto y es un idioma común.
// reproducing your code
class A
state=false; //A
initialized=false; //B
boolean state;
volatile boolean initialized = false; //0
void setState(boolean newState)
state = newState; //1
initialized = true; //2
boolean getState()
if (!initialized) //3
throw ...;
return state; //4
La línea #A #B es un pseudo código para escribir valores predeterminados en variables (también conocido como poner a cero los campos). Necesitamos incluirlos en un análisis estricto. Tenga en cuenta que #B es diferente de # 0; ambos son ejecutados La línea #B no se considera una escritura volátil.
Todos los accesos volátiles (lectura / escritura) en todas las variables están en un orden total. Queremos establecer que el # 2 está antes del # 3 en este orden, si se alcanza el # 4.
Hay 3 escrituras para initialized
: #B, # 0 y # 2. Sólo el # 2 asigna verdadero. Por lo tanto, si # 2 está después de # 3, # 3 no puede leer verdadero (esto probablemente se deba a la garantía de no salir del aire que no entiendo completamente), entonces no se puede alcanzar # 4.
Por lo tanto, si se alcanza el # 4, el # 2 debe estar antes del # 3 (en el orden total de los accesos volátiles).
Por lo tanto, el # 2 sucede antes del # 3 (una escritura volátil ocurre antes de una lectura volátil posterior).
Al programar el orden, # 1 sucede antes de # 2, # 3 sucede antes de # 4.
Por transitividad, por lo tanto, el # 1 sucede antes del # 4.
Línea # A, la escritura predeterminada, sucede antes de todo (excepto otras escrituras predeterminadas)
Por lo tanto, todos los accesos al state
variable están en una cadena de suceso antes: #A -> # 1 -> # 4. No hay carrera de datos. El programa está correctamente sincronizado. Leer # 4 debe observar escribir # 1
Sin embargo, hay un pequeño problema. La línea # 0 es aparentemente redundante, ya que #B ya tiene asignado falso. En la práctica, una escritura volátil no es despreciable en el rendimiento, por lo tanto, debemos evitar el # 0.
Peor aún, la presencia de # 0 puede causar un comportamiento no deseado: ¡# 0 puede ocurrir después de # 2! Por lo tanto, puede suceder que se setState()
, pero getState()
subsiguiente getState()
errores.
Esto es posible si el objeto no se publica de forma segura. Supongamos que el hilo T1 crea el objeto y lo publica; el hilo T2 obtiene el objeto y llama a setState()
en él. Si la publicación no es segura, T2 puede observar la referencia al objeto, antes de que T1 haya terminado de inicializar el objeto.
Puede ignorar este problema si necesita que todos los objetos A
se publiquen de forma segura. Ese es un requisito razonable. Se puede esperar implícitamente.
Pero si no tenemos la línea # 0, esto no será un problema en absoluto. La escritura predeterminada #B debe suceder antes de # 2, por lo tanto, siempre que se setState()
, todos los getState()
subsiguientes se observarán initialized==true
.
En el ejemplo de cuenta regresiva, initialized
es final
; eso es crucial para garantizar una publicación segura: todos los subprocesos observarán un cierre correctamente iniciado.