Perché i ++ non è atomico?


97

Perché i++non è atomico in Java?

Per approfondire Java ho provato a contare la frequenza con cui vengono eseguiti i cicli nei thread.

Quindi ho usato un file

private static int total = 0;

nella classe principale.

Ho due thread.

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

E conto le righe stampate dal thread 1 e dal thread 2. Ma le righe del thread 1 + le righe del thread 2 non corrispondono al numero totale di righe stampate.

Ecco il mio codice:

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();
    }
}

14
Perché non ci provi AtomicInteger?
Braj


3
la JVM ha iincun'operazione per l'incremento di interi, ma funziona solo per le variabili locali, dove la concorrenza non è un problema. Per i campi, il compilatore genera i comandi di lettura-modifica-scrittura separatamente.
Silly Freak

14
Perché dovresti aspettarti che sia atomico?
Hot Licks

2
@Silly Freak: anche se ci fosse iincun'istruzione per i campi, avere una singola istruzione non garantisce l'atomicità, ad esempio l' accesso non volatile longe al doublecampo non è garantito come atomico indipendentemente dal fatto che sia eseguito da una singola istruzione bytecode.
Holger

Risposte:


125

i++probabilmente non è atomico in Java perché l'atomicità è un requisito speciale che non è presente nella maggior parte degli usi di i++. Questo requisito ha un notevole overhead: c'è un grande costo nel rendere atomica un'operazione di incremento; implica la sincronizzazione sia a livello di software che di hardware che non devono essere presenti in un normale incremento.

Si potrebbe fare l'argomento che i++avrebbe dovuto essere progettato e documentato come l'esecuzione specifica di un incremento atomico, in modo che un incremento non atomico venga eseguito utilizzando i = i + 1. Tuttavia, questo interromperà la "compatibilità culturale" tra Java e C e C ++. Inoltre, toglierebbe una comoda notazione che i programmatori che hanno familiarità con i linguaggi C-like danno per scontata, dandole un significato speciale che si applica solo in circostanze limitate.

Il codice C o C ++ di base come for (i = 0; i < LIMIT; i++)si tradurrebbe in Java come for (i = 0; i < LIMIT; i = i + 1); perché sarebbe inappropriato usare l'atomico i++. Quel che è peggio, i programmatori che provengono da C o altri linguaggi simili a C a Java lo userebbero i++comunque, risultando in un uso non necessario di istruzioni atomiche.

Anche a livello di set di istruzioni della macchina, un'operazione di tipo incremento di solito non è atomica per motivi di prestazioni. In x86, un'istruzione speciale "prefisso di blocco" deve essere utilizzata per rendere l' incistruzione atomica: per gli stessi motivi di cui sopra. Se incfosse sempre atomico, non verrebbe mai usato quando è richiesto un inc non atomico; programmatori e compilatori genererebbero codice che carica, aggiunge 1 e memorizza, perché sarebbe molto più veloce.

In alcune architetture di set di istruzioni, non c'è atomica inco forse non c'è incaffatto; per fare un atomic inc su MIPS, devi scrivere un loop software che usa lland sc: load-linked e store-conditional. Load-linked legge la parola e store-conditional memorizza il nuovo valore se la parola non è cambiata, oppure fallisce (il che viene rilevato e provoca un nuovo tentativo).


2
poiché java non ha puntatori, l'incremento delle variabili locali è intrinsecamente un thread save, quindi con i loop il problema per lo più non sarebbe così grave. il tuo punto di vista sul minor numero di sorprese è, ovviamente. inoltre, così com'è, i = i + 1sarebbe una traduzione per ++i, noni++
Silly Freak

22
La prima parola della domanda è "perché". Al momento, questa è l'unica risposta per affrontare la questione del "perché". Le altre risposte in realtà si limitano a ribadire la domanda. Quindi +1.
Dawood ibn Kareem

3
Potrebbe valere la pena notare che una garanzia di atomicità non risolverebbe il problema di visibilità per gli aggiornamenti dei non volatilecampi. Quindi, a meno che non tratterai ogni campo in modo implicito volatileuna volta che un thread ha utilizzato l' ++operatore su di esso, una tale garanzia di atomicità non risolverebbe i problemi di aggiornamento simultaneo. Allora perché potenzialmente sprecare prestazioni per qualcosa se non risolve il problema.
Holger

1
@DavidWallace non intendi ++? ;)
Dan Hlavenka

36

i++ prevede due operazioni:

  1. leggere il valore corrente di i
  2. incrementare il valore e assegnarlo a i

Quando due thread vengono eseguiti i++sulla stessa variabile contemporaneamente, possono entrambi ottenere lo stesso valore corrente di i, quindi incrementarlo e impostarlo su i+1, in modo da ottenere un singolo incremento invece di due.

Esempio :

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

(Anche se i++ fosse atomico, non sarebbe un comportamento ben definito / thread-safe.)
user2864740

15
+1, ma "1. A, 2. B e C" suona come tre operazioni, non due. :)
yshavit

3
Si noti che anche se l'operazione fosse implementata con una singola istruzione della macchina che ha incrementato una posizione di archiviazione sul posto, non vi è alcuna garanzia che sarebbe thread-safe. La macchina deve ancora recuperare il valore, incrementarlo e memorizzarlo, inoltre potrebbero esserci più copie cache di quella posizione di archiviazione.
Hot Licks

3
@Aquarelle - Se due processori eseguono la stessa operazione contemporaneamente sulla stessa posizione di archiviazione e non vi è alcuna trasmissione "di riserva" sulla posizione, allora quasi certamente interferiranno e produrranno risultati fasulli. Sì, è possibile che questa operazione sia "sicura", ma richiede uno sforzo particolare, anche a livello hardware.
Hot Licks

6
Ma penso che la domanda fosse "perché" e non "cosa succede".
Sebastian Mach

11

La cosa importante è la JLS (Java Language Specification) piuttosto che il modo in cui le varie implementazioni della JVM possono o meno aver implementato una certa caratteristica del linguaggio. Il JLS definisce l'operatore postfisso ++ nella clausola 15.14.2 che dice ia "il valore 1 viene aggiunto al valore della variabile e la somma viene nuovamente memorizzata nella variabile". Da nessuna parte menziona o accenna al multithreading o all'atomicità. Per questi il ​​JLS fornisce volatile e sincronizzato . Inoltre, c'è il pacchetto java.util.concurrent.atomic (vedi http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )


5

Perché i ++ non è atomico in Java?

Rompiamo l'operazione di incremento in più istruzioni:

Discussione 1 e 2:

  1. Recupera il valore del totale dalla memoria
  2. Aggiungi 1 al valore
  3. Riscrivi alla memoria

Se non c'è sincronizzazione, diciamo che il thread uno ha letto il valore 3 e lo ha incrementato a 4, ma non l'ha riscritto. A questo punto, avviene il cambio di contesto. Il thread due legge il valore 3, lo incrementa e si verifica il cambio di contesto. Sebbene entrambi i thread abbiano incrementato il valore totale, sarà comunque 4 condizioni di gara.


2
Non capisco come dovrebbe essere una risposta alla domanda. Una lingua può definire qualsiasi caratteristica come atomica, sia che si tratti di incrementi o di unicorni. Semplicemente esemplifica una conseguenza del non essere atomico.
Sebastian Mach

Sì, un linguaggio può definire qualsiasi funzionalità come atomica, ma per quanto java sia considerato operatore di incremento (che è la domanda posta da OP) non è atomica e la mia risposta indica le ragioni.
Aniket Thakur

1
(scusa per il mio tono duro nel primo commento) Ma poi, il motivo sembra essere "perché se fosse atomico, non ci sarebbero le condizioni di gara". Cioè, sembra che una condizione di gara sia desiderabile.
Sebastian Mach

@phresnel l'overhead introdotto per mantenere un incremento atomico è enorme e raramente desiderato, mantenendo l'operazione a buon mercato e di conseguenza non atomico è desiderabile la maggior parte del tempo.
josefx

4
@ josefx: nota che non sto mettendo in dubbio i fatti, ma il ragionamento in questa risposta. Fondamentalmente dice "i ++ non è atomico in Java a causa delle condizioni di gara che ha" , che è come dire "un'auto non ha airbag a causa degli incidenti che possono accadere" o "non ottieni coltello con il tuo ordine di currywurst perché il potrebbe essere necessario tagliare la salsiccia " . Quindi, non penso che questa sia una risposta. La domanda non era "Cosa fa i ++?" o "Qual è la conseguenza della mancata sincronizzazione di i ++?" .
Sebastian Mach

5

i++ è un'istruzione che implica semplicemente 3 operazioni:

  1. Leggere il valore corrente
  2. Scrivi nuovo valore
  3. Memorizza nuovo valore

Queste tre operazioni non sono pensate per essere eseguite in un unico passaggio o in altre parole i++non è un'operazione composta . Di conseguenza, ogni genere di cose può andare storto quando più di un thread è coinvolto in un'operazione singola ma non composta.

Considera il seguente scenario:

Tempo 1 :

Thread A fetches i
Thread B fetches i

Tempo 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)

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

E il gioco è fatto. Una condizione di gara.


Ecco perché i++non è atomico. Se lo fosse, niente di tutto questo sarebbe accaduto e ciascuno fetch-update-storesarebbe accaduto atomicamente. Questo è esattamente ciò che AtomicIntegerserve e nel tuo caso probabilmente si adatterebbe perfettamente.

PS

Un libro eccellente che copre tutti questi problemi e poi alcuni è questo: Java Concurrency in Practice


1
Hmm. Una lingua può definire qualsiasi caratteristica come atomica, sia che si tratti di incrementi o di unicorni. Semplicemente esemplifica una conseguenza del non essere atomico.
Sebastian Mach

@phresnel Esattamente. Ma sottolineo anche che non è una singola operazione, il che per estensione implica che il costo computazionale per trasformare più operazioni di questo tipo in operazioni atomiche è molto più costoso, il che a sua volta giustifica in parte perché i++non è atomico.
kstratis

1
Mentre capisco il tuo punto, la tua risposta è un po 'confusa per l'apprendimento. Vedo un esempio e una conclusione che dice "a causa della situazione nell'esempio"; imho questo è un ragionamento incompleto :(
Sebastian Mach

1
@phresnel Forse non è la risposta più pedagogica, ma è la migliore che posso offrire attualmente. Si spera che aiuti le persone e non le confonda. Grazie per le critiche comunque. Cercherò di essere più preciso nei miei prossimi post.
kstratis

2

Nella JVM, un incremento implica una lettura e una scrittura, quindi non è atomico.


2

Se l'operazione i++fosse atomica non avresti la possibilità di leggerne il valore. Questo è esattamente quello che vuoi fare usando i++(invece di usare ++i).

Ad esempio, guarda il codice seguente:

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

In questo caso ci aspettiamo che l'output sia: 0 (perché inseriamo l'incremento, ad esempio prima leggi, poi aggiorna)

Questo è uno dei motivi per cui l'operazione non può essere atomica, perché è necessario leggere il valore (e fare qualcosa con esso) e quindi aggiornare il valore.

L'altro motivo importante è che fare qualcosa in modo atomico di solito richiede più tempo a causa del blocco. Sarebbe sciocco che tutte le operazioni sulle primitive impiegassero un po 'più di tempo per i rari casi in cui le persone vogliono avere operazioni atomiche. È per questo che hanno aggiunto AtomicIntegere altre classi atomica alla lingua.


2
Questo è fuorviante. Devi separare l'esecuzione e ottenere il risultato, altrimenti non potresti ottenere valori da nessuna operazione atomica.
Sebastian Mach

No, non lo è, ecco perché AtomicInteger di Java ha get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () ecc.
Roy van Rijn

1
E il linguaggio Java avrebbe potuto essere definito i++per essere espanso i.getAndIncrement(). Tale espansione non è nuova. Ad esempio, i lambda in C ++ vengono espansi in definizioni di classi anonime in C ++.
Sebastian Mach

Dato un atomico i++si può banalmente creare un atomico ++io viceversa. Uno è equivalente all'altro più uno.
David Schwartz

2

Ci sono due passaggi:

  1. recupera i dalla memoria
  2. impostare i + 1 su i

quindi non è un'operazione atomica. Quando thread1 esegue i ++ e thread2 esegue i ++, il valore finale di i può essere i + 1.


-1

La concorrenza (la Threadclasse e simili) è una funzionalità aggiunta nella v1.0 di Java . i++è stato aggiunto nella versione beta prima, e come tale è ancora più che probabile nella sua (più o meno) implementazione originale.

Spetta al programmatore sincronizzare le variabili. Dai un'occhiata al tutorial di Oracle su questo .

Modifica: per chiarire, i ++ è una procedura ben definita che precede Java, e come tale i progettisti di Java hanno deciso di mantenere la funzionalità originale di quella procedura.

L'operatore ++ è stato definito in B (1969), che precede java e threading di un po '.


-1 "thread della classe pubblica ... Dal momento che: JDK1.0" Fonte: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak

La versione non importa tanto quanto il fatto che fosse ancora implementata prima della classe Thread e non sia stata modificata a causa di ciò, ma ho modificato la mia risposta per farti piacere.
TheBat

5
Ciò che conta è che la tua affermazione "era ancora implementata prima della classe Thread" non è supportata dai sorgenti. i++non essere atomici è una decisione progettuale, non una svista in un sistema in crescita.
Silly Freak

Lol che è carino. i ++ è stato definito ben prima di Threads, semplicemente perché c'erano linguaggi che esistevano prima di Java. I creatori di Java hanno utilizzato questi altri linguaggi come base invece di ridefinire una procedura ben accettata. Dove ho mai detto che era una svista?
TheBat

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.