Diferencia entre volátil y sincronizado en Java

233

Me pregunto la diferencia entre declarar una variable como volatiley acceder siempre a la variable en un synchronized(this)bloque en Java.

De acuerdo con 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 bloquear: solo estamos haciendo una simple lectura o escritura, por lo que, a diferencia de un bloque sincronizado, nunca mantendremos ningún bloqueo;
  • porque 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 en volatilelugar de acceder a ellas a través de un synchronizedbloque? ¿Es una buena idea usar volatilepara variables que dependen de la entrada? Por ejemplo, ¿hay una variable llamada renderque se lee a través del bucle de representación y se establece mediante un evento de pulsación de tecla?

Albus Dumbledore
fuente

Respuestas:

383

Es importante comprender que la seguridad de los hilos tiene dos aspectos.

  1. control de ejecución, y
  2. visibilidad de 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 puede ejecutarse 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 hilos. 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 cualquier momento dado porque los subprocesos pueden obtener y trabajar en copias privadas de la memoria principal.

El uso synchronizedevita que cualquier otro hilo 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 "sucede antes", lo que provoca una restricción de visibilidad de la memoria de tal manera que cualquier cosa que se haga hasta el punto en que un subproceso libera un bloqueo aparece en otro subproceso que posteriormente adquiere el mismo bloqueo antes de adquirir el bloqueo. En términos prácticos, en el hardware actual, esto generalmente provoca el enjuague de los cachés de la CPU cuando se adquiere un monitor y se escribe en la memoria principal cuando se libera, los cuales son (relativamente) caros.

El uso volatile, por otro lado, obliga a todos los accesos (lectura o escritura) a la variable volátil a ocurrir en la memoria principal, manteniendo efectivamente la variable volátil fuera de los cachés de la CPU. Esto puede ser útil para algunas acciones en las que simplemente se requiere que la visibilidad de la variable sea correcta y el orden de acceso no sea importante. El uso volatiletambién cambia el tratamiento longy doublerequiere que los accesos a ellos sean atómicos; en algunos hardware (más antiguo) esto podría requerir bloqueos, aunque no en el 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 ser casi tan fuerte como sincronizada con respecto a la visibilidad de la memoria y el orden de las instrucciones (ver http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile) Para fines de visibilidad, cada acceso a un campo volátil actúa como la mitad de una sincronización.

Bajo el nuevo modelo de memoria, todavía es cierto que las variables volátiles no pueden reordenarse entre sí. La diferencia es que ahora ya no es tan fácil reordenar los accesos de campo normales a su alrededor. Escribir en un campo volátil tiene el mismo efecto de memoria que un lanzamiento de monitor, y leer desde un campo volátil tiene el mismo efecto de memoria que adquiere un monitor. En efecto, debido a que el nuevo modelo de memoria impone restricciones más estrictas al reordenamiento de los accesos de campo volátiles con otros accesos de campo, volátiles o no, todo lo que era visible para el hilo Acuando escribe en el campo volátil se fvuelve visible para el hilo Bcuando lee f.

- 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 evita que el compilador o el tiempo de ejecución reordene las instrucciones a través de la barrera. En el antiguo JMM, la volatilidad no impedía reordenar. Esto puede ser importante, porque aparte de las barreras de memoria, la única limitación impuesta es que, para cualquier subproceso en particular , el efecto neto del código es el mismo que sería si las instrucciones se ejecutaran exactamente 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, con muchos otros hilos tomando una referencia al objeto en un punto particular de su ciclo de ejecución. Uno necesita que los otros subprocesos comiencen a usar el objeto recreado una vez que se publica, pero no necesita la sobrecarga adicional de la sincronización completa y su contención auxiliar y el vaciado de caché.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing 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.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
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 a su pregunta de lectura-actualización-escritura, específicamente. Considere el siguiente código inseguro:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Ahora, con el método updateCounter () no sincronizado, pueden ingresar dos subprocesos al mismo tiempo. Entre las muchas permutaciones de lo que podría suceder, una es que el subproceso-1 realiza la prueba para el contador == 1000 y lo encuentra verdadero y luego se suspende. Entonces thread-2 hace la misma prueba y también lo ve verdadero y se suspende. Luego se reanuda el subproceso 1 y se establece el contador en 0. Luego se reanuda el subproceso 2 y nuevamente se establece el contador en 0 porque se perdió la actualización del subproceso 1. Esto también puede suceder incluso si el cambio de subproceso no ocurre como lo he descrito, sino simplemente porque dos copias diferentes de contador en caché estaban presentes en dos núcleos de CPU diferentes y cada subproceso se ejecutó en un núcleo separado. Para el caso, un hilo podría tener un contador en un valor y el otro podría tener un contador en un valor completamente diferente solo por el almacenamiento en caché.

Lo importante en este ejemplo es que el contador variable 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 más tarde, cuando se produjo una barrera de memoria o cuando la memoria caché era necesaria para otra cosa. Hacer el contador volatilees insuficiente para la seguridad del hilo de este código, porque la prueba para el máximo y las asignaciones son operaciones discretas, incluido el incremento que es un conjunto de read+increment+writeinstrucciones de máquina no atómicas , algo como:

MOV EAX,counter
INC EAX
MOV counter,EAX

Las variables volátiles son útiles solo cuando todas las operaciones realizadas en ellas son "atómicas", como en mi ejemplo en el que una referencia a un objeto completamente formado solo se lee o se escribe (y, de hecho, normalmente solo se escribe desde un único punto). Otro ejemplo sería una referencia de matriz volátil que respalde una lista de copia en escritura, siempre que la matriz solo se lea tomando primero una copia local de la referencia.

Lawrence Dol
fuente
55
¡Muchas gracias! El ejemplo con el contador es simple de entender. Sin embargo, cuando las cosas se vuelven reales, es un poco diferente.
Albus Dumbledore
"En términos prácticos, en el hardware actual, esto generalmente provoca el enjuague de las memorias caché de la CPU cuando se adquiere un monitor y escribe en la memoria principal cuando se libera, los cuales son caros (en términos relativos)". . Cuando dice cachés de CPU, ¿es lo mismo que Java Stacks local para cada hilo? ¿O un hilo tiene su propia versión local de Heap? Disculpa si estoy siendo tonto aquí.
NishM
1
@nishm No es lo mismo, pero incluiría los cachés locales de los hilos involucrados. .
Lawrence Dol
1
@ MarianPaździoch: Un incremento o decremento NO es una lectura o una escritura, es una lectura y una escritura; es una lectura en un registro, luego un incremento de registro, luego una escritura en la memoria. Las lecturas y escrituras son individualmente atómicas, pero múltiples operaciones de este tipo no lo son.
Lawrence Dol
2
Entonces, de acuerdo con las preguntas frecuentes, no solo las acciones realizadas desde la adquisición de un bloqueo se hacen visibles después del desbloqueo, sino que todas las acciones realizadas por ese hilo se hacen visibles. Incluso acciones realizadas antes de la adquisición de la cerradura.
Lii
97

volatile es un modificador de campo , mientras que sincronizado modifica bloques de código y métodos . Por lo tanto, 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 almacenado actualmente i1en 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, otro subproceso puede haberse actualizado i1en 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 para 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 para i1, para el hilo 1 tenga un valor de 2 para i1y para el hilo 2tener un valor de 3 para i1si thread1 e thread2 han actualizado i1 pero esos valores actualizados aún no se han propagado a la memoria "principal" u otros hilos.

Por otro lado, geti2()accede efectivamente al valor de i2desde la memoria "principal". No se permite que una variable volátil tenga una copia local de una variable que sea diferente del valor actualmente guardado en la memoria "principal". Efectivamente, una variable declarada volátil debe tener sus datos sincronizados en todos los subprocesos, de modo que siempre que acceda o actualice la variable en cualquier subproceso, todos los demás subprocesos ven inmediatamente el mismo valor. En general, las variables volátiles tienen una mayor sobrecarga de acceso y actualización que las variables "simples". En general, los hilos pueden tener su propia copia de datos para una mejor eficiencia.

Hay dos diferencias entre volitil y sincronizado.

En primer lugar, sincronizado obtiene y libera bloqueos en los monitores que pueden forzar solo un subproceso a la vez para 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 hilo con la memoria "principal". Entonces ejecutar geti3()hace lo siguiente:

  1. El hilo adquiere el bloqueo en el monitor para objetar esto.
  2. La memoria de subprocesos vacía todas sus variables, es decir, tiene todas sus variables efectivamente leídas de la memoria "principal".
  3. Se ejecuta el bloque de código (en este caso, establecer el valor de retorno al valor actual de i3, que puede haberse restablecido desde la memoria "principal").
  4. (Cualquier cambio en las variables normalmente ahora se escribiría en la memoria "principal", pero para geti3 () no tenemos cambios).
  5. El hilo libera el bloqueo en el monitor para objetar esto.

Entonces, cuando volátil solo sincroniza el valor de una variable entre la memoria del hilo y la memoria "principal", sincronizado sincroniza el valor de todas las variables entre la memoria del hilo y la memoria "principal", y bloquea y libera un monitor para arrancar. Claramente sincronizado es probable que tenga más sobrecarga que volátil.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

Kerem Baydoğan
fuente
35
-1, Volatile no adquiere un bloqueo, utiliza la arquitectura de CPU subyacente para garantizar la visibilidad en todos los hilos después de la escritura.
Michael Barker
Vale la pena señalar que puede haber algunos casos en los que se puede usar un candado para garantizar la atomicidad de las escrituras. Por ejemplo, escribir un largo en una plataforma de 32 bits que no admite derechos de ancho extendido. Intel evita esto mediante el uso de registros SSE2 (128 bits de ancho) para manejar largos volátiles. Sin embargo, considerar un volátil como bloqueo probablemente provocará errores desagradables en su código.
Michael Barker
2
La semántica importante que comparten las cerraduras y las variables volátiles es que ambas proporcionan bordes Happens-Before (Java 1.5 y posterior). Entrar en un bloque sincronizado, sacar un bloqueo y leer un volátil se considera como un "adquirir" y la liberación de un bloqueo, salir de un bloque sincronizado y escribir un volátil son todas formas de un "lanzamiento".
Michael Barker
20

synchronizedes un modificador de restricción de acceso a nivel de bloque / nivel de método. Se asegurará de que un hilo posea el bloqueo para la sección crítica. Solo el hilo, que posee una cerradura, puede ingresar al synchronizedbloque. Si otros hilos intentan acceder a esta sección crítica, deben esperar hasta que el propietario actual libere el bloqueo.

volatilees un modificador de acceso variable que obliga a todos los hilos a obtener el último valor de la variable de la memoria principal. No se requiere bloqueo para acceder a las volatilevariables. Todos los hilos pueden acceder al valor variable volátil al mismo tiempo.

Un buen ejemplo para usar variable volátil: Datevariable.

Suponga que ha hecho que la Fecha sea variable volatile. 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 un valor de fecha real (real). No necesita hilos diferentes que muestren tiempos diferentes para la misma variable. Todos los hilos deben mostrar el valor de fecha correcto.

ingrese la descripción de la imagen aquí

Eche un vistazo a este artículo para comprender mejor el volatileconcepto.

Lawrence Dol explicó claramente tu read-write-update query.

En cuanto a sus otras consultas

¿Cuándo es más adecuado declarar variables volátiles que acceder a ellas de forma sincronizada?

volatileDebe usarlo si cree que todos los hilos deberían obtener el valor real de la variable en tiempo real como en el ejemplo que he explicado para la variable Fecha.

¿Es una buena idea usar volátil para variables que dependen de la entrada?

La respuesta será la misma que en la primera consulta.

Consulte este artículo para una mejor comprensión.

Ravindra babu
fuente
Entonces, la lectura puede ocurrir al mismo tiempo, y todos los subprocesos leerán el último valor porque la CPU no almacena en caché la memoria principal en la caché de subprocesos de la CPU, pero ¿qué pasa con la escritura? ¿Escribir no debe ser concurrente correcto? Segunda pregunta: si un bloque está sincronizado, pero la variable no es volátil, el valor de una variable en un bloque sincronizado aún puede ser cambiado por otro hilo en otro bloque de código, ¿verdad?
the_prole
11

tl; dr :

Hay 3 problemas principales con multihilo:

1) Condiciones de carrera

2) Caché / memoria obsoleta

3) Cumplidor y optimizaciones de CPU

volatilepuede resolver 2 y 3, pero no puede resolver 1. synchronized/ los bloqueos explícitos pueden resolver 1, 2 y 3.

Elaboración :

1) Considere este código inseguro de hilo:

x++;

Si bien puede parecer una operación, en realidad es 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 xoriginalmente era 1, después de que 2 hilos operen el código, puede ser 2 y puede ser 3, dependiendo de qué hilo completó qué parte de la operación antes de que el control se transfiriera al otro hilo. Esta es una forma de condición de carrera .

El uso synchronizedde un bloque de código lo hace atómico , lo que significa que las 3 operaciones suceden a la vez, y no hay forma de que otro hilo se interponga en el medio. Así que si xera 1 y 2 hilos tratan de preformas x++que sabemos que al final será igual a 3. Por lo que resuelve el problema de condición de carrera.

synchronized (this) {
   x++; // no problem now
}

Calificación x como volatileno hace x++;atómico, por lo que no resuelve este problema.

2) Además, los subprocesos tienen su propio contexto, es decir, pueden almacenar en caché los valores de 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.

Tengamos en cuenta que en un hilo, x = 10;. Y un poco más tarde, en otro hilo, x = 20;. Es posible que el cambio en el valor de xno aparezca en el primer subproceso, porque el otro subproceso ha guardado el nuevo valor en su memoria de trabajo, pero no lo ha copiado en la memoria principal. O que lo copió a la memoria principal, pero el primer hilo no ha actualizado su copia de trabajo. Entonces, si ahora el primer hilo verificaif (x == 20) la respuesta será false.

Marcar una variable como volatilebásicamente le dice a todos los hilos que hagan operaciones de lectura y escritura solo en la memoria principal.synchronizedle dice a cada subproceso que actualice su valor de la memoria principal cuando ingresan al bloque y que devuelva el resultado 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 descargas en la memoria principal.

3) El compilador y la CPU pueden (sin ninguna forma de sincronización entre subprocesos) tratar todo el código como subproceso único. Lo que significa que puede ver algún código, que es muy significativo en un aspecto de subprocesos múltiples, y tratarlo como si fuera 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 de él por completo, si no sabe que este código está diseñado para funcionar en múltiples hilos.

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);
    }
}

Pensaría que threadB solo podría imprimir 20 (o no imprimir nada en absoluto si se ejecuta threadB if-check antes de establecerlo ben verdadero), ya que bse establece en verdadero solo después de que xse establece en 20, pero el compilador / CPU podría decidir reordenar threadA, en ese caso, threadB también podría imprimir 10. Marcado bcomo volatileasegura que no se reordenará (o descartará en ciertos casos). Lo que significa que threadB solo podría imprimir 20 (o nada en absoluto). Marcar los métodos como sincronizados logrará el mismo resultado. También marcando una variable comovolatile solo asegura que no se reordenará, pero todo antes / después aún se puede reordenar, por lo que la sincronización puede ser más adecuada en algunos escenarios.

Tenga en cuenta que antes de Java 5 New Memory Model, volatile no resolvía este problema.

David Refaeli
fuente
1
"Si bien puede parecer una operación, en realidad es 3: leer el valor actual de x de la memoria, agregarle 1 y guardarlo nuevamente en la memoria". - Correcto, porque los valores de la memoria deben pasar por los circuitos de la CPU para poder agregarlos / modificarlos. Aunque esto solo se convierte en una sola INCoperación de ensamblaje , las operaciones subyacentes de la CPU siguen siendo 3 veces mayores y requieren bloqueo para la seguridad del hilo. Buen punto. Aunque, los INC/DECcomandos se pueden marcar atómicamente en el ensamblaje y seguir siendo 1 operación atómica.
Zombis
@ Zombies, así que cuando creo un bloque sincronizado para x ++, ¿lo convierte en un INC / DEC atómico marcado o utiliza un bloqueo normal?
David Refaeli
¡No lo sé! Lo que sí sé es que INC / DEC no son atómicos porque para una CPU, tiene que cargar el valor y LEERlo y también ESCRIBIRLO (en la memoria), como cualquier otra operación aritmética.
Zombies