Diferencia entre volátil y sincronizado en Java
Me pregunto cuál es la diferencia entre declarar una variable como volatile
y acceder siempre a la variable en un synchronized(this)
bloque en Java.
Según este artículo http://www.javamex.com/tutorials/synchronization_volatile.shtml hay mucho que decir y hay muchas diferencias pero también algunas similitudes.
Estoy particularmente interesado en esta información:
...
- el acceso a una variable volátil nunca tiene el potencial de bloquearse: solo realizamos una lectura o escritura simple, por lo que, a diferencia de un bloque sincronizado, nunca mantendremos ningún bloqueo;
- debido a que el acceso a una variable volátil nunca mantiene un bloqueo, no es adecuado para casos en los que queremos leer, actualizar y escribir como una operación atómica (a menos que estemos preparados para "perder una actualización");
¿Qué quieren decir con lectura-actualización-escritura ? ¿No es una escritura también una actualización o simplemente significan que la actualización es una escritura que depende de la lectura?
Sobre todo, ¿cuándo es más adecuado declarar variables volatile
en lugar de acceder a ellas a través de un synchronized
bloque? ¿Es una buena idea utilizarlo volatile
para variables que dependen de la entrada? Por ejemplo, ¿hay una variable llamada render
que se lee a través del bucle de representación y se establece mediante un evento de pulsación de tecla?
Es importante comprender que existen dos aspectos de la seguridad de los subprocesos.
- control de ejecución y
- visibilidad de la memoria
El primero tiene que ver con controlar cuándo se ejecuta el código (incluido el orden en que se ejecutan las instrucciones) y si se puede ejecutar simultáneamente, y el segundo tiene que ver con cuándo los efectos en la memoria de lo que se ha hecho son visibles para otros subprocesos. Debido a que cada CPU tiene varios niveles de caché entre ella y la memoria principal, los subprocesos que se ejecutan en diferentes CPU o núcleos pueden ver la "memoria" de manera diferente en un momento dado porque los subprocesos pueden obtener y trabajar en copias privadas de la memoria principal.
El uso synchronized
evita que cualquier otro subproceso obtenga el monitor (o bloqueo) para el mismo objeto , evitando así que todos los bloques de código protegidos por sincronización en el mismo objeto se ejecuten simultáneamente. La sincronización también crea una barrera de memoria de "sucede antes", lo que provoca una restricción de visibilidad de la memoria de modo que cualquier cosa realizada hasta el momento en que un subproceso libera un bloqueo le parece a otro subproceso que posteriormente adquiere el mismo bloqueo que ocurrió antes de que adquiriera el bloqueo. En términos prácticos, en el hardware actual, esto normalmente provoca que se vacíen las cachés de la CPU cuando se adquiere un monitor y se escribe en la memoria principal cuando se libera, los cuales son (relativamente) costosos.
El uso volatile
, por otro lado, fuerza que todos los accesos (lectura o escritura) a la variable volátil se realicen en la memoria principal, manteniendo efectivamente la variable volátil fuera de los cachés de la CPU. Esto puede resultar útil para algunas acciones donde simplemente se requiere que la visibilidad de la variable sea correcta y el orden de los accesos no sea importante. El uso volatile
también cambia el tratamiento long
y double
exige que los accesos a ellos sean atómicos; en algunos hardware (antiguos) esto puede requerir bloqueos, aunque no en hardware moderno de 64 bits. Bajo el nuevo modelo de memoria (JSR-133) para Java 5+, la semántica de volátil se ha fortalecido para que sea casi tan fuerte como sincronizada con respecto a la visibilidad de la memoria y el orden de las instrucciones (consulte http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile ). A efectos de visibilidad, cada acceso a un campo volátil actúa como media sincronización.
Según el nuevo modelo de memoria, sigue siendo cierto que las variables volátiles no se pueden reordenar entre sí. La diferencia es que ahora ya no es tan fácil reordenar los accesos normales a los campos en torno a ellos. Escribir en un campo volátil tiene el mismo efecto de memoria que una liberación del monitor, y leer desde un campo volátil tiene el mismo efecto de memoria que una adquisición del monitor. En efecto, debido a que el nuevo modelo de memoria impone restricciones más estrictas en el reordenamiento de los accesos a campos volátiles con otros accesos a campos, volátiles o no, todo lo que era visible para el subproceso
A
cuando escribe en un campo volátilf
se vuelve visible para el subprocesoB
cuando leef
.-- Preguntas frecuentes sobre JSR 133 (modelo de memoria Java)
Entonces, ahora ambas formas de barrera de memoria (bajo el JMM actual) causan una barrera de reordenamiento de instrucciones que impide que el compilador o el tiempo de ejecución reordenen las instrucciones a través de la barrera. En el antiguo JMM, la volatilidad no impedía el reordenamiento. Esto puede ser importante, porque aparte de las barreras de la memoria, la única limitación impuesta es que, para cualquier hilo en particular , el efecto neto del código es el mismo que sería si las instrucciones se ejecutaran precisamente en el orden en que aparecen en el fuente.
Un uso de volátil es que un objeto compartido pero inmutable se recrea sobre la marcha, y muchos otros subprocesos toman una referencia al objeto en un punto particular de su ciclo de ejecución. Se necesitan otros subprocesos para comenzar a usar el objeto recreado una vez que se publica, pero no se necesita la sobrecarga adicional de la sincronización completa y la consiguiente contención y vaciado de caché.
// Declaration
public class SharedLocation {
static public volatile SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Hablando específicamente de su pregunta de lectura, actualización y escritura. Considere el siguiente código inseguro:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Ahora, con el método updateCounter() no sincronizado, dos subprocesos pueden ingresar al mismo tiempo. Entre las muchas permutaciones de lo que podría suceder, una es que el hilo-1 haga la prueba para el contador==1000 y lo encuentre verdadero y luego se suspenda. Luego, el subproceso 2 hace la misma prueba y también lo ve verdadero y se suspende. Luego, el subproceso 1 se reanuda y establece el contador en 0. Luego, el subproceso 2 se reanuda y nuevamente establece el contador en 0 porque perdió la actualización del subproceso 1. Esto también puede suceder incluso si el cambio de subprocesos no ocurre como lo he descrito, sino simplemente porque dos copias diferentes del contador en caché estaban presentes en dos núcleos de CPU diferentes y cada subproceso se ejecutó en un núcleo separado. De hecho, un subproceso podría tener un contador con un valor y el otro podría tener un contador con un valor completamente diferente simplemente debido al almacenamiento en caché.
Lo importante en este ejemplo es que el contador de variables se leyó de la memoria principal a la memoria caché, se actualizó en la memoria caché y solo se volvió a escribir en la memoria principal en algún momento indeterminado posterior cuando se produjo una barrera de memoria o cuando la memoria caché fue necesaria para otra cosa. Hacer el contador volatile
es insuficiente para la seguridad de subprocesos de este código, porque la prueba del máximo y las asignaciones son operaciones discretas, incluido el incremento, que es un conjunto de instrucciones de máquina no atómicas read+increment+write
, algo como:
MOV EAX,counter
INC EAX
MOV counter,EAX
Las variables volátiles son útiles sólo cuando todas las operaciones realizadas en ellas son "atómicas", como en mi ejemplo donde una referencia a un objeto completamente formado sólo se lee o escribe (y, de hecho, normalmente sólo se escribe desde un único punto). Otro ejemplo sería una referencia de matriz volátil que respalda una lista de copia en escritura, siempre que la matriz solo se leyera tomando primero una copia local de la referencia.
volátil es un modificador de campo , mientras que sincronizado modifica bloques de código y métodos . Entonces podemos especificar tres variaciones de un descriptor de acceso simple usando esas dos palabras clave:
int i1; int geti1() {return i1;} volatile int i2; int geti2() {return i2;} int i3; synchronized int geti3() {return i3;}
geti1()
accede al valor actualmente almacenado eni1
el hilo actual. Los subprocesos pueden tener copias locales de variables y los datos no tienen que ser los mismos que los datos contenidos en otros subprocesos. En particular, es posible que otro subproceso se haya actualizado eni1
su subproceso, pero el valor en el subproceso actual podría ser diferente de ese. valor actualizado. De hecho, Java tiene la idea de una memoria "principal", y esta es la memoria que contiene el valor "correcto" actual de las variables. Los subprocesos pueden tener su propia copia de datos para variables y la copia del subproceso puede ser diferente de la memoria "principal". De hecho, es posible que la memoria "principal" tenga un valor de 1 parai1
, que thread1 tenga un valor de 2 yi1
que thread2 tenga un valor de 3 si thread1 y thread2i1
han actualizado i1 pero esos El valor actualizado aún no se ha propagado a la memoria "principal" ni a otros subprocesos.Por otro lado,
geti2()
accede efectivamente al valor dei2
desde la memoria "principal". No se permite que una variable volátil tenga una copia local de una variable que sea diferente del valor que se mantiene actualmente en la memoria "principal". Efectivamente, una variable declarada volátil debe tener sus datos sincronizados en todos los subprocesos, de modo que cada vez que acceda o actualice la variable en cualquier subproceso, todos los demás subprocesos vean inmediatamente el mismo valor. Generalmente, las variables volátiles tienen una mayor sobrecarga de acceso y actualización que las variables "simples". Generalmente, a los subprocesos se les permite tener su propia copia de datos para una mayor eficiencia.Hay dos diferencias entre volátil y sincronizado.
En primer lugar, sincronizado obtiene y libera bloqueos en los monitores que pueden forzar solo un subproceso a la vez a ejecutar un bloque de código. Ese es el aspecto bastante conocido de sincronizado. Pero sincronizado también sincroniza la memoria. De hecho, sincronizado sincroniza toda la memoria del subproceso con la memoria "principal". Entonces ejecutar
geti3()
hace lo siguiente:
- El hilo adquiere el bloqueo en el monitor para el objeto this.
- La memoria del subproceso vacía todas sus variables, es decir, todas sus variables se leen efectivamente desde la memoria "principal".
- El bloque de código se ejecuta (en este caso, configurando el valor de retorno al valor actual de i3, que puede haber sido restablecido desde la memoria "principal").
- (Cualquier cambio en las variables normalmente se escribiría en la memoria "principal", pero para geti3() no tenemos cambios).
- El hilo libera el bloqueo en el monitor para este objeto.
Entonces, donde volatile solo sincroniza el valor de una variable entre la memoria del subproceso y la memoria "principal", sincronizado sincroniza el valor de todas las variables entre la memoria del subproceso y la memoria "principal", y bloquea y libera un monitor para arrancar. Es probable que lo claramente sincronizado tenga más gastos generales que lo volátil.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
Hay 3 problemas principales con el subproceso múltiple:
Condiciones de carrera
Almacenamiento en caché/memoria obsoleta
Optimizaciones del compilador y la CPU.
volatile
puede resolver 2 y 3, pero no puede resolver 1. synchronized
/los bloqueos explícitos pueden resolver 1, 2 y 3.
Elaboración :
- Considere este hilo como código inseguro:
x++;
Si bien puede parecer una operación, en realidad son 3: leer el valor actual de x de la memoria, agregarle 1 y guardarlo nuevamente en la memoria. Si pocos subprocesos intentan hacerlo al mismo tiempo, el resultado de la operación no está definido. Si x
originalmente era 1, después de que 2 subprocesos ejecuten el código, puede ser 2 o 3, dependiendo de qué subproceso completó qué parte de la operación antes de que el control se transfiriera al otro subproceso. Esta es una forma de condición de carrera .
El uso synchronized
en un bloque de código lo hace atómico , lo que significa que hace que las 3 operaciones ocurran a la vez, y no hay forma de que otro hilo se interponga en el medio e interfiera. Entonces, si x
era 1 y 2 subprocesos intentan realizar la ejecución, x++
sabemos que al final será igual a 3. Entonces resuelve el problema de la condición de carrera.
synchronized (this) {
x++; // no problem now
}
Marcar x
como volatile
no lo hace x++;
atómico, por lo que no resuelve este problema.
- Además, los hilos tienen su propio contexto, es decir, pueden almacenar en caché valores desde la memoria principal. Eso significa que algunos subprocesos pueden tener copias de una variable, pero operan en su copia de trabajo sin compartir el nuevo estado de la variable entre otros subprocesos.
Considere eso en un hilo, x = 10;
. Y algo más adelante, en otro hilo, x = 20;
. Es posible que el cambio de valor de x
no aparezca en el primer hilo, porque el otro hilo guardó el nuevo valor en su memoria de trabajo, pero no lo copió en la memoria principal. O que lo copió en la memoria principal, pero el primer hilo no actualizó su copia de trabajo. Entonces, si ahora el primer hilo verifica, if (x == 20)
la respuesta será false
.
Marcar una variable como volatile
básicamente le dice a todos los subprocesos que realicen operaciones de lectura y escritura solo en la memoria principal. synchronized
le dice a cada hilo que actualice su valor desde la memoria principal cuando ingrese al bloque y que descargue el resultado de regreso a la memoria principal cuando salga del bloque.
Tenga en cuenta que, a diferencia de las carreras de datos, la memoria obsoleta no es tan fácil de (re)producir, ya que de todos modos se producen vaciados a la memoria principal.
- El compilador y la CPU pueden (sin ningún tipo de sincronización entre subprocesos) tratar todo el código como de un solo subproceso. Lo que significa que puede mirar algún código, que es muy significativo en un aspecto de subprocesos múltiples, y tratarlo como si fuera de un solo subproceso, donde no es tan significativo. Por lo tanto, puede mirar un código y decidir, en aras de la optimización, reordenarlo o incluso eliminar partes del mismo por completo, si no sabe que este código está diseñado para funcionar en múltiples subprocesos.
Considere el siguiente código:
boolean b = false;
int x = 10;
void threadA() {
x = 20;
b = true;
}
void threadB() {
if (b) {
System.out.println(x);
}
}
Se podría pensar que threadB solo podría imprimir 20 (o no imprimir nada en absoluto si threadB if-check se ejecuta antes de establecerse b
en verdadero), ya que b
se establece en verdadero solo después de x
establecerse en 20, pero el compilador/CPU podría decidir reordenar hiloA, en ese caso el hiloB también podría imprimir 10. Marcar b
como volatile
garantiza que no se reordenará (o descartará en ciertos casos). Lo que significa que threadB solo pudo imprimir 20 (o nada en absoluto). Marcar los métodos como sincronizados logrará el mismo resultado. Además, marcar una variable como volatile
solo garantiza que no se reordenará, pero todo lo anterior o posterior aún se puede reordenar, por lo que la sincronización puede ser más adecuada en algunos escenarios.
Tenga en cuenta que antes del nuevo modelo de memoria de Java 5, volatile no resolvía este problema.
sincronizado
es una palabra clave utilizada para proteger un método o bloque de código. Al hacer que el método esté sincronizado, logras dos cosas.
- Dos ejecuciones de
synchronized
métodos en el mismo objeto nunca se ejecutan- El cambio en el estado del objeto es visible para otros hilos.
volatile
es un modificador de acceso a variables que obliga a todos los subprocesos a obtener el último valor de la variable de la memoria principal. Todos los subprocesos pueden acceder al valor de la variable volátil al mismo tiempo sin ningún bloqueo.
Un buen ejemplo para utilizar una variable volátil: Date
variable.
Supongamos que ha hecho que la fecha sea variable volatile
. No necesita diferentes hilos que muestren diferentes horas para la misma variable. Todos los subprocesos que acceden a esta variable siempre obtienen los datos más recientes de la memoria principal para que todos los subprocesos muestren el valor de fecha real (actual).
Lawrence Dol explicó claramente su read-write-update query
.
Respecto a tus otras consultas
¿Cuándo es más adecuado declarar variables volátiles que acceder a ellas mediante sincronizadas?
Debe usarlo volatile
si cree que todos los subprocesos deberían obtener el valor real de la variable en tiempo real, como en el ejemplo de datos que se explicó anteriormente.
¿Es una buena idea utilizar volátiles para variables que dependen de la entrada?
La respuesta será la misma que en la primera consulta.