¿Cuál es la diferencia entre atómico/volátil/sincronizado?
¿Cómo funcionan internamente atómico/volátil/sincronizado?
¿Cuál es la diferencia entre los siguientes bloques de código?
Código 1
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Código 2
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Código 3
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
¿ Funciona volatile
de la siguiente manera? Es
volatile int i = 0;
void incIBy5() {
i += 5;
}
equivalente a
Integer i = 5;
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Creo que dos hilos no pueden entrar en un bloque sincronizado al mismo tiempo... ¿verdad? Si esto es cierto, ¿cómo funciona atomic.incrementAndGet()
sin él synchronized
? ¿Y es seguro para subprocesos?
¿Y cuál es la diferencia entre lectura interna y escritura en variables volátiles/variables atómicas? Leí en algún artículo que el hilo tiene una copia local de las variables, ¿qué es eso?
Estás preguntando específicamente cómo funcionan internamente , así que aquí tienes:
Sin sincronización
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Básicamente lee el valor de la memoria, lo incrementa y lo devuelve a la memoria. Esto funciona en un solo subproceso, pero hoy en día, en la era de los cachés de múltiples núcleos, múltiples CPU y múltiples niveles, no funcionará correctamente. En primer lugar, introduce condiciones de carrera (varios subprocesos pueden leer el valor al mismo tiempo), pero también problemas de visibilidad. Es posible que el valor solo se almacene en la memoria de la CPU " local " (algunos caché) y no sea visible para otras CPU/núcleos (y, por lo tanto, subprocesos). Es por eso que muchos se refieren a la copia local de una variable en un hilo. Es muy inseguro. Considere este código de detención de subprocesos popular pero roto:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Agregue volatile
a stopped
la variable y funciona bien: si cualquier otro subproceso modifica stopped
la variable mediante pleaseStop()
el método, tiene la garantía de ver ese cambio inmediatamente en el while(!stopped)
bucle del subproceso de trabajo. Por cierto, esta tampoco es una buena forma de interrumpir un hilo; consulte: Cómo detener un hilo que se ejecuta eternamente sin ningún uso y Detener un hilo java específico .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
La AtomicInteger
clase utiliza operaciones de CPU de bajo nivel CAS ( comparar e intercambiar ) (¡no se necesita sincronización!). Le permiten modificar una variable particular solo si el valor actual es igual a otra cosa (y se devuelve correctamente). Entonces, cuando lo ejecuta getAndIncrement()
, en realidad se ejecuta en un bucle (implementación real simplificada):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Básicamente: leer; intente almacenar el valor incrementado; Si no tiene éxito (el valor ya no es igual a current
), lea e intente nuevamente. Se compareAndSet()
implementa en código nativo (ensamblado).
volatile
sin sincronización
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Este código no es correcto. Soluciona el problema de visibilidad ( volatile
se asegura de que otros hilos puedan ver los cambios realizados counter
) pero aún tiene una condición de carrera. Esto se ha explicado varias veces: el incremento previo/posterior no es atómico.
El único efecto secundario volatile
es " vaciar " los cachés para que todas las demás partes vean la versión más reciente de los datos. Esto es demasiado estricto en la mayoría de las situaciones; por eso volatile
no es predeterminado.
volatile
sin sincronización (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
El mismo problema que el anterior, pero aún peor porque i
no lo es private
. La condición de carrera todavía está presente. Por qué es un problema? Si, por ejemplo, dos subprocesos ejecutan este código simultáneamente, el resultado podría ser + 5
o + 10
. Sin embargo, tiene la garantía de ver el cambio.
Múltiples independientessynchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Sorpresa, este código también es incorrecto. De hecho, está completamente equivocado. En primer lugar, estás sincronizando en i
, que está a punto de cambiarse (además, i
es una primitiva, así que supongo que estás sincronizando en un temporal Integer
creado mediante autoboxing...) Completamente defectuoso. También podrías escribir:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
No pueden entrar dos hilos en el mismo synchronized
bloque con el mismo candado . En este caso (y de manera similar en su código), el objeto de bloqueo cambia en cada ejecución, por lo que synchronized
efectivamente no tiene ningún efecto.
Incluso si ha utilizado una variable final (o this
) para la sincronización, el código sigue siendo incorrecto. Primero, dos subprocesos pueden leer i
de temp
forma sincrónica (teniendo el mismo valor localmente temp
), luego el primero asigna un nuevo valor i
(digamos, de 1 a 6) y el otro hace lo mismo (de 1 a 6).
La sincronización debe abarcar desde la lectura hasta la asignación de un valor. Tu primera sincronización no tiene ningún efecto (la lectura de an int
es atómica) y la segunda tampoco. En mi opinión estas son las formas correctas:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}
Declarar una variable como volátil significa que modificar su valor afecta inmediatamente el almacenamiento de memoria real de la variable. El compilador no puede optimizar ninguna referencia hecha a la variable. Esto garantiza que cuando un hilo modifica la variable, todos los demás hilos ven el nuevo valor inmediatamente. (Esto no está garantizado para variables no volátiles).
Declarar una variable atómica garantiza que las operaciones realizadas en la variable ocurran de forma atómica, es decir, que todos los subpasos de la operación se completen dentro del subproceso en el que se ejecutan y no sean interrumpidos por otros subprocesos. Por ejemplo, una operación de incremento y prueba requiere que la variable se incremente y luego se compare con otro valor; una operación atómica garantiza que ambos pasos se completarán como si fueran una única operación indivisible/ininterrumpible.
La sincronización de todos los accesos a una variable permite que solo un subproceso a la vez acceda a la variable y obliga a todos los demás subprocesos a esperar a que ese subproceso de acceso libere su acceso a la variable.
El acceso sincronizado es similar al acceso atómico, pero las operaciones atómicas generalmente se implementan en un nivel inferior de programación. Además, es completamente posible sincronizar sólo algunos accesos a una variable y permitir que otros accesos no estén sincronizados (por ejemplo, sincronizar todas las escrituras en una variable pero ninguna de las lecturas de ella).
La atomicidad, la sincronización y la volatilidad son atributos independientes, pero normalmente se usan en combinación para imponer una cooperación adecuada entre subprocesos para acceder a las variables.
Anexo (abril de 2016)
El acceso sincronizado a una variable generalmente se implementa mediante un monitor o semáforo . Estos son mecanismos mutex (exclusión mutua) de bajo nivel que permiten que un subproceso adquiera el control de una variable o bloque de código exclusivamente, lo que obliga a todos los demás subprocesos a esperar si también intentan adquirir el mismo mutex. Una vez que el hilo propietario libera el mutex, otro hilo puede adquirir el mutex a su vez.
Anexo (julio de 2016)
La sincronización se produce en un objeto . Esto significa que llamar a un método sincronizado de una clase bloqueará el this
objeto de la llamada. Los métodos sincronizados estáticos bloquearán el Class
objeto en sí.
Asimismo, ingresar a un bloque sincronizado requiere bloquear el this
objeto del método.
Esto significa que un método (o bloque) sincronizado se puede ejecutar en varios subprocesos al mismo tiempo si están bloqueando diferentes objetos, pero solo un subproceso puede ejecutar un método (o bloque) sincronizado a la vez para un solo objeto determinado.
volátil:
volatile
es una palabra clave. volatile
obliga a todos los subprocesos a obtener el último valor de la variable de la memoria principal en lugar del caché. Todos los subprocesos pueden acceder al valor de la variable volátil al mismo tiempo sin bloqueo.
Reduce el error de coherencia de la memoria.
Cuándo usarlo: un hilo modifica los datos y otros hilos tienen que leer el último valor de los datos. Otros hilos tomarán alguna acción sin actualizar los datos .
AtómicoXXX :
AtomicXXX
Las clases admiten programación segura para subprocesos sin bloqueo en variables individuales.
Estas AtomicXXX
clases (como AtomicInteger
) resuelven errores de inconsistencia de memoria.
Cuándo usarlo: varios subprocesos pueden leer y modificar datos.
sincronizado :
synchronized
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.
Cuándo usarlo: varios subprocesos pueden leer y modificar datos. Su lógica de negocios no solo actualiza los datos sino que también ejecuta operaciones atómicas.
AtomicXXX
es equivalente a volatile + synchronized
aunque la implementación sea diferente.
AmtomicXXX
extiende volatile
variables + compareAndSet
métodos pero no utiliza sincronización.
Sé que dos subprocesos no pueden ingresar al bloque Sincronizar al mismo tiempo
Dos subprocesos no pueden ingresar dos veces a un bloque sincronizado en el mismo objeto. Esto significa que dos subprocesos pueden ingresar al mismo bloque en diferentes objetos. Esta confusión puede llevar a un código como este.
private Integer i = 0;
synchronized(i) {
i++;
}
Esto no se comportará como se esperaba, ya que podría bloquearse en un objeto diferente cada vez.
Si esto es cierto, ¿cómo funciona este atomic.incrementAndGet() sin sincronizar? ¿Y es seguro para subprocesos?
Sí. No utiliza bloqueo para lograr la seguridad del hilo.
Si quieres saber cómo funcionan con más detalle, puedes leer el código correspondiente.
¿Y cuál es la diferencia entre lectura interna y escritura en variable volátil/variable atómica?
La clase atómica utiliza campos volátiles. No hay diferencia en el campo. La diferencia son las operaciones realizadas. Las clases Atomic utilizan operaciones CompareAndSwap o CAS.
Leí en algún artículo que el hilo tiene una copia local de variables, ¿qué es eso?
Sólo puedo suponer que se refiere al hecho de que cada CPU tiene su propia vista de memoria caché que puede ser diferente de cualquier otra CPU. Para garantizar que su CPU tenga una vista coherente de los datos, debe utilizar técnicas de seguridad de subprocesos.
Esto solo es un problema cuando la memoria se comparte y al menos un hilo la actualiza.