¿Por qué i++ no es atómico?
¿ Por qué no es i++
atómico en Java?
Para profundizar un poco más en Java, intenté contar con qué frecuencia se ejecuta el bucle en los subprocesos.
Así que usé un
private static int total = 0;
en la clase principal.
Tengo dos hilos.
- Hilo 1: Impresiones
System.out.println("Hello from Thread 1!");
- Hilo 2: Impresiones
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 + las 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 1! 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();
}
}
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 requisito tiene una sobrecarga significativa: hay un gran costo al hacer que una operación incremental sea atómica; Implica sincronización tanto a nivel de software como de hardware que no necesita estar presente en un incremento ordinario.
Se podría argumentar que i++
debería haber sido diseñado y documentado para realizar específicamente 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, C y C++. Además, eliminaría una notación conveniente que los programadores familiarizados con lenguajes tipo C dan por sentada, dándole un significado especial que se aplica sólo en circunstancias limitadas.
El código básico C o C++ for (i = 0; i < LIMIT; i++)
se traduciría a Java como for (i = 0; i < LIMIT; i = i + 1)
; porque sería inapropiado utilizar el atómico i++
. Lo que es peor, los programadores que vienen de C u otros lenguajes similares a C y usan Java i++
de todos modos, lo que resulta en un uso innecesario de instrucciones atómicas.
Incluso en el nivel del conjunto de instrucciones de la máquina, una operación de tipo incremental no suele ser atómica por motivos de rendimiento. En x86, se debe utilizar una instrucción especial "prefijo de bloqueo" para hacer que la inc
instrucción sea atómica: por las mismas razones que antes. Si inc
siempre fuera atómico, nunca se usaría cuando se requiera un inc no atómico; Los 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 ninguna atómica inc
o quizás ninguna inc
; Para hacer un inc atómico en MIPS, debe escribir un bucle de software que use and ll
: sc
vinculado a carga y condicional de almacenamiento. El vinculado a la carga lee la palabra y el condicional de almacenamiento almacena el nuevo valor si la palabra no ha cambiado o falla (lo que se detecta y provoca un nuevo intento).
i++
implica dos operaciones:
- leer el valor actual de
i
- incrementar el valor y asignarlo a
i
Cuando dos subprocesos actúan 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 único incremento 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