threads thread practice example concurrent java multithreading concurrency

practice - java thread pool



¿Por qué i++ no es atómico? (9)

¿Por qué i ++ no es atómico en Java?

Vamos a dividir la operación de incremento en varias declaraciones:

Tema 1 y 2:

  1. Obtener valor de total de la memoria
  2. Agrega 1 al valor
  3. Escribir de nuevo en la memoria

Si no hay sincronización, digamos que el subproceso uno ha leído el valor 3 y lo ha incrementado a 4, pero no lo ha escrito nuevamente. En este punto, el cambio de contexto ocurre. El subproceso dos lee el valor 3, lo incrementa y se produce el cambio de contexto. Aunque ambos subprocesos han incrementado el valor total, seguirá siendo 4 - condición de carrera.

¿Por qué i++ no es atómico en Java?

Para profundizar un poco más en Java, traté de contar la frecuencia con la que se ejecuta el ciclo en los hilos.

Entonces usé un

private static int total = 0;

en la clase principal.

Tengo dos hilos.

  • Subproceso 1: Imprime System.out.println("Hello from Thread 1!");
  • Subproceso 2: Imprime System.out.println("Hello from Thread 2!");

Y cuento las líneas impresas por el hilo 1 y el hilo 2. Pero las líneas del hilo 1 + líneas del hilo 2 no coinciden con el número total de líneas impresas.

Aquí está mi código:

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; public class Test { private static int total = 0; private static int countT1 = 0; private static int countT2 = 0; private boolean run = true; public Test() { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); newCachedThreadPool.execute(t1); newCachedThreadPool.execute(t2); try { Thread.sleep(1000); } catch (InterruptedException ex) { Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex); } run = false; try { Thread.sleep(1000); } catch (InterruptedException ex) { Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex); } System.out.println((countT1 + countT2 + " == " + total)); } private Runnable t1 = new Runnable() { @Override public void run() { while (run) { total++; countT1++; System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total); } } }; private Runnable t2 = new Runnable() { @Override public void run() { while (run) { total++; countT2++; System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total); } } }; public static void main(String[] args) { new Test(); } }


Concurrencia (la clase Thread y tal) es una característica adicional en v1.0 de Java . i++ fue agregado en la versión beta antes de eso, y como tal todavía es más que probable en su implementación (más o menos) original.

Depende del programador sincronizar las variables. Consulte el tutorial de Oracle sobre esto .

Editar: para aclarar, i ++ es un procedimiento bien definido que es anterior a Java, y como tal, los diseñadores de Java decidieron mantener la funcionalidad original de ese procedimiento.

El operador ++ se definió en B (1969), que es anterior a java y al hilo con solo un tad.


En la JVM, un incremento implica lectura y escritura, por lo que no es atómico.


Hay dos pasos:

  1. buscarlo de memoria
  2. establecer i + 1 a i

así que no es una operación atómica. Cuando thread1 ejecuta i ++, y thread2 ejecuta i ++, el valor final de i puede ser i + 1.


Lo importante es el JLS (Especificación del lenguaje Java) en lugar de cómo varias implementaciones de la JVM pueden o no haber implementado una determinada característica del lenguaje. El JLS define el operador ++ postfix en la cláusula 15.14.2 que dice ia "el valor 1 se agrega al valor de la variable y la suma se almacena de nuevo en la variable". En ninguna parte menciona o insinúa en multihilo o atomicidad. Para estos, JLS ofrece servicios volátiles y sincronizados . Además, está el paquete java.util.concurrent.atomic (ver http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )


Si la operación i++ fuera atómica, no tendrías la oportunidad de leer su valor. Esto es exactamente lo que quiere hacer con i++ (en lugar de usar ++i ).

Por ejemplo, mira el siguiente código:

public static void main(final String[] args) { int i = 0; System.out.println(i++); }

En este caso, esperamos que el resultado sea: 0 (porque publicamos el incremento, por ejemplo, primero lee, luego actualiza)

Esta es una de las razones por las que la operación no puede ser atómica, porque necesita leer el valor (y hacer algo con él) y luego actualizar el valor.

La otra razón importante es que hacer algo atómicamente generalmente lleva más tiempo debido al bloqueo. Sería una tontería tener todas las operaciones en primitivas un poco más para los raros casos en que la gente quiere tener operaciones atómicas. Es por eso que agregaron AtomicInteger y http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html clases atómicas al lenguaje.


i++ es una afirmación que simplemente implica 3 operaciones:

  1. Leer el valor actual
  2. Escribir nuevo valor
  3. Almacenar nuevo valor

Estas tres operaciones no deben ejecutarse en un solo paso, en otras palabras, i++ no es una operación compuesta . Como resultado, todo tipo de cosas pueden salir mal cuando hay más de un subproceso involucrado en una operación única pero no compuesta.

Como ejemplo imagine este escenario:

Hora 1 :

Thread A fetches i Thread B fetches i

Hora 2 :

Thread A overwrites i with a new value say -foo- Thread B overwrites i with a new value say -bar- Thread B stores -bar- in i // At this time thread B seems to be more ''active''. Not only does it overwrite // its local copy of i but also makes it in time to store -bar- back to // ''main'' memory (i)

Hora 3 :

Thread A attempts to store -foo- in memory effectively overwriting the -bar- value (in i) which was just stored by thread B in Time 2. Thread B has nothing to do here. Its work was done by Time 2. However it was all for nothing as -bar- was eventually overwritten by another thread.

Y ahí lo tienes. Una condición de carrera.

Es por eso que i++ no es atómico. Si lo fuera, nada de esto habría sucedido y cada fetch-update-store ocurriría atómicamente. Para AtomicInteger es exactamente para eso y, en su caso, probablemente encajaría perfectamente.

PD

Un libro excelente que cubre todos estos temas y algo más es esto: Simultaneidad de Java en la práctica


i++ implica dos operaciones:

  1. lea el valor actual de i
  2. incrementar el valor y asignarlo a i

Cuando dos hilos realizan i++ en la misma variable al mismo tiempo, ambos pueden obtener el mismo valor actual de i , y luego incrementarlo y establecerlo en i+1 , por lo que obtendrá un incremento individual en lugar de dos.

Ejemplo:

int i = 5; Thread 1 : i++; // reads value 5 Thread 2 : i++; // reads value 5 Thread 1 : // increments i to 6 Thread 2 : // increments i to 6 // i == 6 instead of 7


i++ probablemente no sea atómico en Java porque la atomicidad es un requisito especial que no está presente en la mayoría de los usos de i++ . Ese requerimiento tiene una sobrecarga significativa: hay un gran costo al hacer una operación incremental atómica; implica la sincronización tanto a nivel de software como de hardware que no necesitan estar presentes en un incremento ordinario.

Podría hacer que el argumento de que i++ debería haberse diseñado y documentado como específicamente realizar un incremento atómico, de modo que se realice un incremento no atómico usando i = i + 1 . Sin embargo, esto rompería la "compatibilidad cultural" entre Java y C y C ++. Además, quitaría una notación conveniente que los programadores familiarizados con los lenguajes tipo C dan por hecho, dándole un significado especial que se aplica solo en circunstancias limitadas.

El código básico C o C ++ como for (i = 0; i < LIMIT; i++) se traduciría en Java como for (i = 0; i < LIMIT; i = i + 1) ; porque sería inapropiado usar el i++ atómico. Lo que es peor, los programadores que vienen de C u otros lenguajes similares a C usarían i++ todos modos, lo que resultaría en un uso innecesario de las instrucciones atómicas.

Incluso en el nivel del conjunto de instrucciones de la máquina, una operación de tipo de incremento generalmente no es atómica por razones de rendimiento. En x86, se debe usar una instrucción especial "prefijo de bloqueo" para hacer que la instrucción inc atómica: por las mismas razones que arriba. Si inc era siempre atómico, nunca se usaría cuando se requiere inc no atómico; programadores y compiladores generarían código que carga, agrega 1 y almacena, porque sería mucho más rápido.

En algunas arquitecturas de conjuntos de instrucciones, no hay inc atomic o quizás no inc en absoluto; para hacer un inc atomic en MIPS, debe escribir un bucle de software que use ll y sc : load-linked y store-conditional. Load-linked lee la palabra, y store-conditional almacena el nuevo valor si la palabra no ha cambiado, o de lo contrario falla (que se detecta y hace que se vuelva a intentar).