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

97

¿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();
    }
}
Andie2302
fuente
14
¿Por qué no lo intentas AtomicInteger?
Braj
3
la JVM tiene una iincoperació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.
Silly Freak
14
¿Por qué esperarías que fuera atómico?
Hot Licks
2
@Silly Freak: incluso si había una iincinstrucción para los campos, que tiene una sola instrucción no garantiza la atomicidad, por ejemplo, no volatile longy doubleel 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.
Holger

Respuestas:

125

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 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 usando i = 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 como for (i = 0; i < LIMIT; i = i + 1); porque sería inapropiado usar el atómico i++. Lo que es peor, los programadores que vienen de C u otros lenguajes similares a C a Java lo usarían de i++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 incinstrucción sea atómica: por las mismas razones que las anteriores. Si incfuera 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 inco tal vez no existe inc; Para hacer un inc atómico en MIPS, debe escribir un bucle de software que use lly sc: 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).

Kaz
fuente
2
como java no tiene punteros, incrementar las variables locales es inherentemente guardar subprocesos, por lo que con los bucles el problema en general no sería tan malo. su punto sobre la menor sorpresa se destaca, por supuesto. también, como está, i = i + 1sería una traducción para ++i, noi++
Silly Freak
22
La primera palabra de la pregunta es "por qué". A partir de ahora, esta es la única respuesta para abordar la cuestión del "por qué". Las otras respuestas realmente simplemente replantean la pregunta. Entonces +1.
Dawood ibn Kareem
3
Vale la pena señalar que una garantía de atomicidad no resolvería el problema de visibilidad de las actualizaciones de los no volatilecampos. Entonces, a menos que trate cada campo como implícitamente volatileuna 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?
Holger
1
@DavidWallace, ¿no te refieres ++? ;)
Dan Hlavenka
36

i++ implica dos operaciones:

  1. leer el valor actual de i
  2. Incrementar el valor y asignarlo a 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 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
Eran
fuente
(Incluso si i++ fuera atómico, no sería un comportamiento bien definido / seguro para subprocesos).
user2864740
15
+1, pero "1. A, 2. B y C" suena como tres operaciones, no dos. :)
yshavit
3
Tenga en cuenta que incluso si la operación se implementara con una sola instrucción de máquina que incrementara una ubicación de almacenamiento en su lugar, no hay garantía de que sea segura para subprocesos. La máquina aún necesita recuperar el valor, incrementarlo y almacenarlo, además de que puede haber múltiples copias en caché de esa ubicación de almacenamiento.
Hot Licks
3
@Aquarelle: si dos procesadores ejecutan la misma operación en la misma ubicación de almacenamiento simultáneamente y no hay transmisión de "reserva" en la ubicación, es casi seguro que interferirán y producirán resultados falsos. Sí, es posible que esta operación sea "segura", pero requiere un esfuerzo especial, incluso a nivel de hardware.
Hot Licks
6
Pero creo que la pregunta era "por qué" y no "qué pasa".
Sebastian Mach
11

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 )

Jonathan Rosenne
fuente
5

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

Dividamos la operación de incremento en varias declaraciones:

Hilo 1 y 2:

  1. Obtener el valor del total de la memoria
  2. Suma 1 al valor
  3. Escribe de nuevo en la memoria

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.

Aniket Thakur
fuente
2
No entiendo cómo debería ser esto una respuesta a la pregunta. Un lenguaje puede definir cualquier característica como atómica, ya sean incrementos o unicornios. Simplemente ejemplifica una consecuencia de no ser atómico.
Sebastian Mach
Sí, un lenguaje puede definir cualquier característica como atómica, pero en la medida en que java se considere operador de incremento (que es la pregunta publicada por OP) no es atómico y mi respuesta indica las razones.
Aniket Thakur
1
(perdón por mi tono severo en el primer comentario) Pero luego, la razón parece ser "porque si fuera atómico, entonces no habría condiciones de carrera". Es decir, parece que una condición de carrera es deseable.
Sebastian Mach
@phresnel la sobrecarga introducida para mantener un incremento atómico es enorme y rara vez se desea, mantener la operación barata y, como resultado, no atómica es deseable la mayor parte del tiempo.
josefx
4
@josefx: Tenga en cuenta que no estoy cuestionando los hechos, sino el razonamiento en esta respuesta. Básicamente dice "i ++ no es atómico en Java debido a las condiciones de carrera que tiene" , que es como decir "un automóvil no tiene airbag debido a los choques que pueden ocurrir" o "no obtienes un cuchillo con tu currywurst-order porque el puede ser necesario cortar la salchicha " . Por lo tanto, no creo que esta sea una respuesta. La pregunta no era "¿Qué hace i ++?" o "¿Cuál es la consecuencia de que i ++ no se sincronice?" .
Sebastian Mach
5

i++ es una declaración que simplemente involucra 3 operaciones:

  1. Leer valor actual
  2. Escribe un nuevo valor
  3. Almacenar nuevo valor

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 :

Thread A fetches i
Thread B fetches i

Tiempo 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.


Por eso i++no es atómico. Si lo fuera, nada de esto habría sucedido y cada uno fetch-update-storesucederí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

kstratis
fuente
1
Hmm. Un lenguaje puede definir cualquier característica como atómica, ya sean incrementos o unicornios. Simplemente ejemplifica una consecuencia de no ser atómico.
Sebastian Mach
@phresnel Exactamente. Pero también señalo que no es una sola operación lo que por extensión implica que el costo computacional de convertir múltiples operaciones de este tipo en atómicas es mucho más costoso, lo que a su vez -en parte- justifica por qué i++no es atómico.
kstratis
1
Si bien entiendo su punto, su respuesta es un poco confusa para el aprendizaje. Veo un ejemplo y una conclusión que dice "debido a la situación en el ejemplo"; En mi humilde opinión, este es un razonamiento incompleto :(
Sebastian Mach
1
@phresnel Quizás no sea la respuesta más pedagógica, pero es la mejor que puedo ofrecer actualmente. Ojalá ayude a las personas y no las confunda. Sin embargo, gracias por las críticas. Intentaré ser más preciso en mis próximas publicaciones.
kstratis
2

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

celeritas
fuente
2

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 usando i++(en lugar de usar ++i).

Por ejemplo, mire el siguiente código:

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

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 AtomicIntegery otras clases atómicas al idioma.

Roy van Rijn
fuente
2
Esto es engañoso. Debe separar la ejecución y obtener el resultado; de lo contrario, no podría obtener valores de ninguna operación atómica.
Sebastian Mach
No, no lo es, es por eso que AtomicInteger de Java tiene get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet (), etc.
Roy van Rijn
1
Y el lenguaje Java podría haberse definido i++para expandirse i.getAndIncrement(). Tal expansión no es nueva. Por ejemplo, las lambdas en C ++ se expanden a definiciones de clases anónimas en C ++.
Sebastian Mach
Dado un atómico i++uno puede crear trivialmente un atómico ++io viceversa. Uno es equivalente al otro más uno.
David Schwartz
2

Hay dos pasos:

  1. sacarme de la memoria
  2. establecer i + 1 en i

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.

Yanghaogn
fuente
-1

La concurrencia (la Threadclase 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.

El murciélago
fuente
-1 "Hilo de clase pública ... Desde: JDK1.0" Fuente: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak
La versión no importa tanto como el hecho de que todavía se implementó antes de la clase Thread y no se cambió debido a eso, pero he editado mi respuesta para complacerlo.
TheBat
5
Lo que importa es que su afirmación "todavía se implementó antes de la clase Thread" no está respaldada por fuentes. i++no ser atómico es una decisión de diseño, no un descuido en un sistema en crecimiento.
Silly Freak
Lol eso es lindo. i ++ se definió mucho antes que Threads, simplemente porque había lenguajes que existían antes de Java. Los creadores de Java utilizaron esos otros lenguajes como base en lugar de redefinir un procedimiento bien aceptado. ¿Dónde dije alguna vez que fue un descuido?
TheBat
@SillyFreak Aquí hay algunas fuentes que muestran cuántos años tiene ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat