¿Cuál es la diferencia entre atómico / volátil / sincronizado?

¿Cómo funciona el trabajo atómico / volátil / sincronizado internamente?

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

¿ 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 … ¿estoy en lo cierto? Si esto es cierto, ¿cómo funciona atomic.incrementAndGet() sin synchronized ? ¿Y es seguro para subprocesos?

¿Y cuál es la diferencia entre la lectura interna y la escritura en variables volátiles / variables atómicas? Leí en un 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í estás:

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 hilo, pero hoy en día, en la era de los cachés multinúcleo, multi-CPU y multinivel, no funcionará correctamente. En primer lugar, introduce 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 hilos:

 private boolean stopped; public void run() { while(!stopped) { //do some work } } public void pleaseStop() { stopped = true; } 

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

AtomicInteger

 private AtomicInteger counter = new AtomicInteger(); public int getNextUniqueIndex() { return counter.getAndIncrement(); } 

La clase AtomicInteger usa operaciones de CPU de bajo nivel CAS ( compare-and-swap ) (¡no se necesita sincronización!). Permiten modificar una variable en particular solo si el valor presente es igual a otra cosa (y se devuelve con éxito). Entonces cuando ejecutas getAndIncrement() realmente se ejecuta en un bucle (implementación real simplificada):

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

Entonces básicamente: leer; tratar de almacenar el valor incrementado; si no tiene éxito (el valor ya no es igual a la current ), lea y vuelva a intentarlo. 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 ( volatile asegura que otros subprocesos puedan ver el cambio realizado en el counter ) pero todavía tiene una condición de carrera. Esto se ha explicado varias veces: el incremento previo / posterior no es atómico.

El único efecto secundario de la volatile es ” enjuagar ” 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; es por eso que volatile no es el 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 no soy private . La condición de carrera aún está presente. Por qué es un problema? Si, por ejemplo, dos hilos ejecutan este código simultáneamente, la salida puede ser + 5 o + 10 . Sin embargo, está garantizado para 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, es completamente incorrecto. En primer lugar, estás sincronizando en i , que está a punto de cambiarse (además, soy un primitivo, así que supongo que estás sincronizando en un Integer temporal creado a través de un autoboxing …) Completamente defectuoso. También puedes escribir:

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

No pueden entrar dos hilos en el mismo bloque synchronized con el mismo locking . En este caso (y de forma similar en su código), el objeto de locking cambia en cada ejecución, por lo que la synchronized efectiva 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 i a temp sincrónicamente (teniendo el mismo valor localmente en temp ), luego el primero asigna un nuevo valor a i (por ejemplo, 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. Su primera sincronización no tiene ningún efecto (leer un int es atómico) y el segundo 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; } } 

Declarar una variable como volátil significa que modificar su valor afecta inmediatamente el almacenamiento de memoria real para la variable. El comstackdor 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 variables no volátiles).

La statement de una variable atómica garantiza que las operaciones realizadas en la variable ocurren en forma atómica, es decir, que todos los pasos intermedios de la operación se completan dentro del hilo que se ejecutan y no son 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 ambos pasos se completarán como si se tratara de una única operación indivisible / ininterrumpida.

La sincronización de todos los accesos a una variable permite que solo un único subproceso a la vez acceda a la variable, y obliga a todos los demás subprocesos a esperar a que el subproceso acceda para liberar su acceso a la variable.

El acceso sincronizado es similar al acceso atómico, pero las operaciones atómicas generalmente se implementan a un nivel más bajo de progtwigció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 ella).

La atomicidad, la sincronización y la volatilidad son atributos independientes, pero por lo general se usan en combinación para aplicar la cooperación de hilos adecuada para acceder a las variables.

Adición (abril de 2016)

El acceso sincronizado a una variable generalmente se implementa usando un monitor o semáforo . Estos son mecanismos de mutex (exclusión mutua) de bajo nivel que permiten a un subproceso adquirir el control exclusivo de una variable o bloque de código, lo que obliga al rest de 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 por turno.

Adición (julio de 2016)

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

Del mismo modo, ingresar un bloque sincronizado requiere bloquear this objeto del método.

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

volátil:

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

El uso de variables volatile reduce el riesgo de errores de consistencia de la memoria, ya que cualquier escritura en una variable volátil establece una relación de pasar antes a las lecturas posteriores de esa misma variable.

Esto significa que los cambios en una variable volatile siempre son visibles para otros hilos . Lo que es más, también significa que cuando un hilo lee una variable volatile , no solo ve el último cambio en el volátil, sino también los efectos secundarios del código que provocó el 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 medida, pero no actualizarán los datos .

AtomicXXX:

AtomicXXX clases AtomicXXX compatibles con la progtwigción sin locking de subprocesos en variables individuales. Estas clases de AtomicXXX (como AtomicInteger ) resuelven los errores de inconsistencia de la memoria / efectos secundarios de la modificación de variables volátiles, a los que se ha accedido en múltiples hilos.

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

sincronizado:

synchronized es la 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. En primer lugar, no es posible intercalar dos invocaciones de métodos synchronized en el mismo objeto. Cuando un hilo está ejecutando un método synchronized para un objeto, todos los otros hilos que invocan métodos synchronized para el mismo bloque de objetos (suspenden la ejecución) hasta que el primer hilo termina con el objeto.

  2. En segundo lugar, cuando sale un método synchronized , establece automáticamente una relación de pasar antes con cualquier invocación posterior de un método synchronized 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 de negocio no solo actualiza los datos sino que también ejecuta operaciones atómicas

AtomicXXX es equivalente a volatile + synchronized a pesar de que la implementación es diferente. AmtomicXXX amplía volatile variables volatile + compareAndSet métodos de compareAndSet pero no usa la sincronización.

Preguntas relacionadas con SE:

Diferencia entre volátil y sincronizado en Java

Volatile boolean 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

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

Dos hilos no pueden ingresar dos veces en un bloque sincronizado en el mismo objeto. Esto significa que dos hilos pueden ingresar al 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 espera, ya que podría estar bloqueando un objeto diferente cada vez.

si esto es cierto, ¿cómo funciona atomic.incrementAndGet () sin sincronizar? y es seguro para hilos?

sí. No usa locking 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 Atomic usan operaciones CompareAndSwap o CAS.

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

Solo puedo suponer que se refiere al hecho de que cada CPU tiene su propia vista de memoria almacenada en caché que puede ser diferente de cualquier otra CPU. Para garantizar que su CPU tenga una vista coherente de los datos, debe usar técnicas de seguridad de subprocesos.

Esto es solo un problema cuando se comparte la memoria, al menos un hilo lo actualiza.

Una sincronización volátil + es una solución infalible para que una operación (statement) 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; que hace i como el valor 3 en la memoria después de la ejecución de esta statement. 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 con el cálculo incrementar el valor existente con uno (2 + 1 = 3 en el acumulador) y luego volver a 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 lectura / escritura ÚNICA de la memoria sea atómica y no con MÚLTIPLES. Por lo tanto, necesitamos tener sincronizado también alrededor de i ++ para mantenerlo como una statement atómica a prueba de tontos. Recuerde el hecho de que una statement incluye múltiples declaraciones.

Espero que la explicación sea lo suficientemente clara.

El modificador volátil Java es un ejemplo de un mecanismo especial para garantizar que la comunicación se realice entre hilos. Cuando un hilo escribe en una variable volátil, y otro hilo ve esa escritura, el primer hilo le dice al segundo sobre todos los contenidos 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 entornos de subprocesos múltiples para evitar la inconsistencia de los datos.