¿Cuál es la diferencia entre atómica / volátil / sincronizada?

297

¿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++; 
}

¿ volatileFunciona 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 ... ¿estoy en lo cierto? Si esto es cierto, ¿cómo atomic.incrementAndGet()funciona 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?

hardik
fuente
55
Eso hace muchas preguntas, con código que ni siquiera se compila. Tal vez deberías leer un buen libro, como Java Concurrency in Practice.
JB Nizet
44
@JBNizet tienes razón !!! Tengo ese libro, no tiene un concepto atómico brevemente y no entiendo algunos conceptos de eso. de maldición es mi error, no de autor.
hardik
44
Realmente no tiene que importarle cómo se implementa (y varía con el sistema operativo). Lo que tiene que entender es el contrato: el valor se incrementa atómicamente y se garantiza que todos los otros subprocesos verán el nuevo valor.
JB Nizet

Respuestas:

392

Usted pregunta específicamente cómo funcionan internamente , así que aquí está:

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 varios núcleos, múltiples CPU y niveles múltiples, no funcionará correctamente. En primer lugar, presenta la condición de carrera (varios hilos pueden leer el valor al mismo tiempo), pero también problemas de visibilidad. El valor solo puede almacenarse en la memoria de la CPU " local " (algo de caché) y no ser 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 popular pero roto código de detención de subprocesos:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Agregue volatilea la stoppedvariable y funciona bien: si cualquier otro hilo modifica la stoppedvariable a través del pleaseStop()método, se garantiza que verá ese cambio inmediatamente en el while(!stopped)bucle del hilo de trabajo . Por cierto, esta no es una buena manera de interrumpir un hilo tampoco, vea: Cómo detener un hilo que se ejecuta para siempre sin ningún uso y Detener un hilo java específico .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

La AtomicIntegerclase usa 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 presente es igual a otra cosa (y se devuelve con éxito). Entonces, cuando lo ejecutas, getAndIncrement()en realidad se ejecuta en un bucle (implementación real simplificada):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Básicamente: lee; intente almacenar valor incrementado; si no tiene éxito (el valor ya no es igual a current), lea e intente nuevamente. El compareAndSet()se 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 ( volatilese 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 volatilees el " vaciado " de 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 volatileno es por defecto.

volatile sin sincronización (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

El mismo problema que el anterior, pero aún peor porque ino lo es private. La condición de carrera aún está presente. Por qué es un problema? Si, digamos, dos hilos ejecutan este código simultáneamente, el resultado podría ser + 5o + 10. Sin embargo, tiene la garantía de ver el cambio.

Múltiple independiente synchronized

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á sincronizando i, lo que está a punto de cambiar (además, ies primitivo, así que supongo que está sincronizando en un temporal Integercreado mediante autoboxing ...) Completamente defectuoso. También podrías escribir:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

No pueden entrar dos hilos en el mismo synchronizedbloque con el mismo bloqueo . En este caso (y de manera similar en su código) el objeto de bloqueo cambia con cada ejecución, por lo que synchronizedefectivamente no tiene ningún efecto.

Incluso si ha utilizado una variable final (o this) para la sincronización, el código sigue siendo incorrecto. Dos hilos pueden leer primero ide forma tempsíncrona (con el mismo valor localmente temp), luego el primero asigna un nuevo valor a i(digamos, del 1 al 6) y el otro hace lo mismo (del 1 al 6).

La sincronización debe abarcar desde la lectura hasta la asignación de un valor. Su primera sincronización no tiene efecto (la lectura de un intes atómica) y la segunda también. 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;
  }
}
Tomasz Nurkiewicz
fuente
10
Lo único que agregaría es que la JVM copia valores variables en registros para operar en ellos. Esto significa que los subprocesos que se ejecutan en una sola CPU / núcleo aún pueden ver diferentes valores para una variable no volátil.
David Harkness
@thomasz: ¿está sincronizado compareAndSet (actual, actual + 1)? si no, ¿qué sucede cuando dos hilos ejecutan este método al mismo tiempo?
hardik
@Hardik: compareAndSetes solo una delgada envoltura alrededor de la operación CAS. Entro en algunos detalles en mi respuesta.
Tomasz Nurkiewicz
1
@thomsasz: ok, reviso esta pregunta de enlace y respondida por jon skeet, él dice "el hilo no puede leer una variable volátil sin verificar si algún otro hilo ha realizado una escritura". ¡Pero qué sucede si un hilo está en el medio de la operación de escritura y el segundo hilo lo está leyendo! Me equivoco ?? ¿no es condición de carrera en operación atómica?
hardik
3
@Hardik: cree otra pregunta para obtener más respuestas sobre lo que está preguntando, aquí solo somos usted y yo, y los comentarios no son apropiados para hacer preguntas. No olvides publicar un enlace a una nueva pregunta aquí para que pueda hacer un seguimiento.
Tomasz Nurkiewicz
61

Declarar una variable como volátil significa que modificar su valor afecta inmediatamente el almacenamiento de memoria real para la variable. El compilador no puede optimizar ninguna referencia hecha a la variable. Esto garantiza que cuando un hilo modifica la variable, todos los otros hilos ven el nuevo valor inmediatamente. (Esto no está garantizado para las variables no volátiles).

La declaración de una variable atómica garantiza que las operaciones realizadas en la variable se realicen de manera atómica, es decir, que todos los subpasos de la operación se completen dentro del hilo que se ejecutan y no sean interrumpidos por otros hilos. 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 estos dos pasos se completarán como si fueran una sola 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 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 solo algunos accesos a una variable y permitir que otros accesos no estén sincronizados (por ejemplo, sincronizar todas las escrituras a una variable pero ninguna de las lecturas de la misma).

La atomicidad, la sincronización y la volatilidad son atributos independientes, pero generalmente se usan en combinación para forzar una cooperación de hilos adecuada 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 de bajo nivel (exclusión mutua) que permiten que un hilo adquiera el control de una variable o bloque de código exclusivamente, obligando a todos los otros hilos a esperar si también intentan adquirir el mismo mutex. Una vez que el subproceso propietario libera el mutex, otro subproceso puede adquirir el mutex a su vez.

Anexo (julio de 2016)

La sincronización ocurre en un objeto . Esto significa que llamar a un método sincronizado de una clase bloqueará el thisobjeto de la llamada. Los métodos estáticos sincronizados bloquearán el Classobjeto en sí.

Del mismo modo, ingresar un bloque sincronizado requiere bloquear el thisobjeto del método.

Esto significa que un método sincronizado (o bloque) puede ejecutarse en múltiples subprocesos al mismo tiempo si se bloquean en diferentes objetos, pero solo un subproceso puede ejecutar un método sincronizado (o bloque) a la vez para un solo objeto dado .

David R Tribble
fuente
25

volátil:

volatilees una palabra clave volatileobliga a todos los subprocesos a obtener el último valor de la variable de la memoria principal en lugar de la memoria caché. No se requiere bloqueo para acceder a variables volátiles. Todos los hilos pueden acceder al valor variable volátil al mismo tiempo.

El uso de volatilevariables reduce el riesgo de errores de consistencia de la memoria, porque cualquier escritura en una variable volátil establece una relación de suceder antes con lecturas posteriores de esa misma variable.

Esto significa que los cambios en una volatilevariable siempre son visibles para otros subprocesos . Además, también significa que cuando un hilo lee una volatilevariable, ve no solo el último cambio en el volátil, sino también los efectos secundarios del código que condujo al cambio .

Cuándo usar: Un hilo modifica los datos y otros hilos tienen que leer el último valor de los datos. Otros hilos tomarán alguna acción, pero no actualizarán los datos .

AtomicXXX:

AtomicXXXLas clases admiten la programación segura y sin subprocesos en variables individuales Estas AtomicXXXclases (como AtomicInteger) resuelven errores de inconsistencia de memoria / efectos secundarios de modificación de variables volátiles, a las que se ha accedido en múltiples subprocesos.

Cuándo usar: múltiples hilos pueden leer y modificar datos.

sincronizado:

synchronizedes una palabra clave utilizada para proteger un método o bloque de código. Al hacer que el método esté sincronizado tiene dos efectos:

  1. Primero, no es posible synchronizedintercalar dos invocaciones de métodos en el mismo objeto. Cuando un subproceso está ejecutando un synchronizedmétodo para un objeto, todos los demás subprocesos que invocan synchronizedmétodos para el mismo objeto bloquean (suspenden la ejecución) hasta que el primer subproceso se realiza con el objeto.

  2. En segundo lugar, cuando synchronizedsale un método, establece automáticamente una relación de suceso anterior con cualquier invocación posterior de un synchronizedmétodo para el mismo objeto. Esto garantiza que los cambios en el estado del objeto sean visibles para todos los hilos.

Cuándo usar: múltiples hilos pueden leer y modificar datos. Su lógica empresarial no solo actualiza los datos sino que también ejecuta operaciones atómicas

AtomicXXXes equivalente volatile + synchronizeda pesar de que la implementación es diferente. AmtomicXXXextiende las volatilevariables + compareAndSetmétodos pero no usa sincronización.

Preguntas SE relacionadas:

Diferencia entre volátil y sincronizado en Java

Boolean volátil vs AtomicBoolean

Buenos artículos para leer: (El contenido anterior se toma de estas páginas de documentación)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

Ravindra babu
fuente
2
Esta es la primera respuesta que realmente menciona la semántica de las palabras clave / características descritas, que son importantes para comprender cómo afectan realmente la ejecución del código. Las respuestas con mayor voto pierden este aspecto.
jhyot
5

Sé que dos hilos no pueden entrar en el bloque Sincronizar al mismo tiempo

Dos hilos no pueden ingresar un bloque sincronizado en el mismo objeto dos veces. Esto significa que dos hilos pueden entrar en el mismo bloque en diferentes objetos. Esta confusión puede conducir 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 que ¿Cómo funciona atomic.incrementAndGet () sin Sincronizar? y es seguro para hilos?

si. No utiliza bloqueo para lograr la seguridad del hilo.

Si desea saber cómo funcionan con más detalle, puede leer el código para ellos.

¿Y cuál es la diferencia entre lectura interna y escritura en Variable volátil / Variable atómica?

La clase atómica usa campos volátiles . No hay diferencia en el campo. La diferencia son las operaciones realizadas. Las clases atómicas usan operaciones CompareAndSwap o CAS.

Leí en algún artículo que el hilo tiene una copia local de variables, ¿qué es eso?

Solo puedo suponer que se refiere al hecho de que cada CPU tiene su propia vista de memoria en 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 es solo un problema cuando la memoria se comparte, al menos un subproceso la actualiza.

Peter Lawrey
fuente
@Aniket Thakur, ¿estás seguro de eso? El entero es inmutable. Por lo tanto, i ++ probablemente desempaquetará automáticamente el valor int, lo incrementará y luego creará un nuevo número entero, que no es la misma instancia que antes. Intenta hacer i final y obtendrás errores de compilación al llamar a i ++.
fuemf5
2

Sincronizado Vs Atómico Vs Volátil:

  • Volátil y Atómico se aplica solo en variable, mientras que Sincronizado se aplica en método.
  • Los volátiles garantizan la visibilidad, no la atomicidad / consistencia del objeto, mientras que otros aseguran la visibilidad y la atomicidad.
  • El almacenamiento de variables volátiles en RAM y el acceso es más rápido, pero no podemos lograr la seguridad o sincronización de subprocesos sin una palabra clave sincronizada.
  • Sincronizado implementado como bloque sincronizado o método sincronizado mientras que ambos no. Podemos enhebrar varias líneas de código seguras con ayuda de palabras clave sincronizadas, mientras que con ambas no podemos lograr lo mismo.
  • Sincronizado puede bloquear el mismo objeto de clase u objeto de clase diferente mientras que ambos no pueden.

Por favor corrígeme si algo me perdí.

Ajay Gupta
fuente
1

Una sincronización volátil + es una solución infalible para que una operación (declaración) sea completamente atómica, lo que incluye múltiples instrucciones para la CPU.

Digamos por ejemplo: volátil int i = 2; i ++, que no es más que i = i + 1; lo que hace que i sea el valor 3 en la memoria después de la ejecución de esta declaración. Esto incluye leer el valor existente de la memoria para i (que es 2), cargar en el registro del acumulador de la CPU y hacer el cálculo incrementando el valor existente con uno (2 + 1 = 3 en el acumulador) y luego escribir ese valor incrementado De vuelta a la memoria. Estas operaciones no son lo suficientemente atómicas, aunque el valor de i es volátil. ser volátil garantiza solo que una SOLA lectura / escritura desde la memoria es atómica y no MÚLTIPLE. Por lo tanto, necesitamos estar sincronizados también alrededor de i ++ para mantenerlo como una declaración atómica infalible. Recuerde el hecho de que una declaración incluye múltiples declaraciones.

Espero que la explicación sea lo suficientemente clara.

Thomas Mathew
fuente
1

El modificador volátil Java es un ejemplo de un mecanismo especial para garantizar que la comunicación ocurra entre hilos. Cuando un hilo escribe en una variable volátil, y otro hilo ve esa escritura, el primer hilo le dice al segundo todo el contenido de la memoria hasta que realiza la escritura en esa variable volátil.

Las operaciones atómicas se realizan en una sola unidad de tarea sin interferencia de otras operaciones. Las operaciones atómicas son necesarias en un entorno de subprocesos múltiples para evitar inconsistencias de datos.

Sai prateek
fuente