¿Por qué i++
no es atómico en Java?
Para profundizar un poco más en Java, traté de contar con qué frecuencia se ejecuta el bucle en los subprocesos.
Entonces 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 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();
}
}
java
multithreading
concurrency
Andie2302
fuente
fuente
AtomicInteger
?iinc
operación para incrementar enteros, pero eso solo funciona para variables locales, donde la concurrencia no es una preocupación. Para los campos, el compilador genera comandos de lectura, modificación y escritura por separado.iinc
instrucción para los campos, que tiene una sola instrucción no garantiza la atomicidad, por ejemplo, novolatile
long
ydouble
el acceso de campo en no garantiza que sea atómica sin tener en cuenta el hecho de que se lleva a cabo por una sola instrucción de código de bytes.Respuestas:
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 dei++
. Ese requisito tiene una sobrecarga significativa: hay un gran costo en 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.Podría hacer el argumento que
i++
debería haber sido diseñado y documentado como que realiza específicamente un incremento atómico, de modo que un incremento no atómico se realice usandoi = i + 1
. Sin embargo, esto rompería la "compatibilidad cultural" entre Java y C y C ++. Además, eliminaría una notación conveniente que los programadores familiarizados con lenguajes similares a C dan por sentada, dándole un significado especial que se aplica solo en circunstancias limitadas.El código básico de C o C ++ como
for (i = 0; i < LIMIT; i++)
se traduciría a Java comofor (i = 0; i < LIMIT; i = i + 1)
; porque sería inapropiado usar el atómicoi++
. Lo que es peor, los programadores que vienen de C u otros lenguajes similares a C a Java lo usarían dei++
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 de incremento 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 las anteriores. Siinc
fuera siempre 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 atómico
inc
o tal vez no existeinc
; Para hacer un inc atómico en MIPS, debe escribir un bucle de software que usell
ysc
: vinculado a la carga y condicional a la tienda. Load-linked lee la palabra y store-conditional almacena el nuevo valor si la palabra no ha cambiado, o falla (lo que se detecta y provoca un reintento).fuente
i = i + 1
sería una traducción para++i
, noi++
volatile
campos. Entonces, a menos que trate cada campo como implícitamentevolatile
una vez que un hilo haya usado el++
operador en él, tal garantía de atomicidad no resolvería los problemas de actualización simultánea. Entonces, ¿por qué desperdiciar potencialmente el rendimiento en algo si no resuelve el problema?++
? ;)i++
implica dos operaciones:i
i
Cuando dos subprocesos funcionan
i++
en la misma variable al mismo tiempo, ambos pueden obtener el mismo valor actual dei
, y luego incrementarlo y establecerlo eni+1
, por lo que obtendrá un único incremento en lugar de dos.Ejemplo:
fuente
i++
fuera atómico, no sería un comportamiento bien definido / seguro para subprocesos).Lo importante es la 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 de sufijo ++ 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 el multiproceso o la atomicidad. Para estos el JLS proporciona volátiles y sincronizados . Además, está el paquete java.util.concurrent.atomic (consulte http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )
fuente
Dividamos la operación de incremento en varias declaraciones:
Hilo 1 y 2:
Si no hay sincronización, digamos que el hilo uno ha leído el valor 3 y lo ha incrementado a 4, pero no lo ha escrito. En este punto, ocurre el cambio de contexto. El hilo dos lee el valor 3, lo incrementa y ocurre el cambio de contexto. Aunque ambos subprocesos han incrementado el valor total, seguirá siendo condición de 4 carreras.
fuente
i++
es una declaración que simplemente involucra 3 operaciones:Estas tres operaciones no están diseñadas para ejecutarse en un solo paso o, en otras palabras,
i++
no es un compuesto operación . Como resultado, todo tipo de cosas pueden salir mal cuando más de un subproceso está involucrado en una operación única pero no compuesta.Considere el siguiente escenario:
Hora 1 :
Tiempo 2 :
Hora 3 :
Y ahí lo tienes. Una condición de carrera.
Por eso
i++
no es atómico. Si lo fuera, nada de esto habría sucedido y cada unofetch-update-store
sucedería atómicamente. Eso es exactamente lo queAtomicInteger
y en su caso probablemente encajaría bien.PD
Un libro excelente que cubre todos esos temas y algunos más es este: Concurrencia de Java en la práctica
fuente
i++
no es atómico.En la JVM, un incremento implica una lectura y una escritura, por lo que no es atómico.
fuente
Si la operación
i++
fuera atómica, no tendría la oportunidad de leer el valor de ella. Esto es exactamente lo que quiere hacer usandoi++
(en lugar de usar++i
).Por ejemplo, mire el siguiente código:
En este caso, esperamos que la salida sea:
0
(porque publicamos un incremento, por ejemplo, primero leemos, luego actualizamos)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 de forma atómica suele llevar más tiempo debido al bloqueo. Sería una tontería que todas las operaciones en primitivas demoren un poco más en los raros casos en que la gente quiere tener operaciones atómicas. Por eso han agregado
AtomicInteger
y otras clases atómicas al idioma.fuente
i++
para expandirsei.getAndIncrement()
. Tal expansión no es nueva. Por ejemplo, las lambdas en C ++ se expanden a definiciones de clases anónimas en C ++.i++
uno puede crear trivialmente un atómico++i
o viceversa. Uno es equivalente al otro más uno.Hay dos pasos:
por lo 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.
fuente
La concurrencia (la
Thread
clase y tal) es una característica adicional en la v1.0 de Java .i++
se agregó en la versión beta antes de eso, y como tal, es más que probable que esté en su implementación original (más o menos).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 subprocesos por solo un poco.
fuente
i++
no ser atómico es una decisión de diseño, no un descuido en un sistema en crecimiento.