Warum ist i ++ nicht atomar?

97

Warum ist i++Java nicht atomar?

Um etwas tiefer in Java einzusteigen, habe ich versucht zu zählen, wie oft die Schleife in Threads ausgeführt wird.

Also habe ich eine verwendet

private static int total = 0;

in der Hauptklasse.

Ich habe zwei Threads.

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

Und ich zähle die Zeilen, die von Thread 1 und Thread 2 gedruckt wurden. Aber die Zeilen von Thread 1 + Zeilen von Thread 2 stimmen nicht mit der Gesamtzahl der ausgedruckten Zeilen überein.

Hier ist mein Code:

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
quelle
14
Warum versuchst du es nicht mit AtomicInteger?
Braj
3
Die JVM verfügt über eine iincOperation zum Inkrementieren von Ganzzahlen, die jedoch nur für lokale Variablen funktioniert, bei denen die Parallelität keine Rolle spielt. Für Felder generiert der Compiler Lese-, Änderungs- und Schreibbefehle separat.
Dumme Freak
14
Warum würden Sie überhaupt erwarten, dass es atomar ist?
Hot Licks
2
@Silly Freak: Selbst wenn es eine iincAnweisung für Felder gab, garantiert eine einzelne Anweisung keine Atomizität, z. B. Nicht- volatile longund Feldzugriff ist doublenicht garantiert atomar, unabhängig davon, dass sie von einer einzelnen Bytecode-Anweisung ausgeführt wird.
Holger

Antworten:

125

i++ist in Java wahrscheinlich nicht atomar, da Atomizität eine spezielle Anforderung ist, die in den meisten Anwendungen von nicht vorhanden ist i++. Diese Anforderung hat einen erheblichen Overhead: Es ist mit hohen Kosten verbunden, eine Inkrementierungsoperation atomar zu machen. Es beinhaltet die Synchronisation sowohl auf Software- als auch auf Hardwareebene, die nicht in einem normalen Inkrement vorhanden sein müssen.

Sie könnten das Argument, i++das entworfen und dokumentiert werden sollte, als spezifisch für die Ausführung eines atomaren Inkrements festlegen, damit ein nicht-atomares Inkrement mit ausgeführt wird i = i + 1. Dies würde jedoch die "kulturelle Kompatibilität" zwischen Java und C und C ++ beeinträchtigen. Außerdem würde es eine bequeme Notation wegnehmen, die Programmierer, die mit C-ähnlichen Sprachen vertraut sind, für selbstverständlich halten, und ihr eine besondere Bedeutung geben, die nur unter bestimmten Umständen gilt.

Grundlegender C- oder C ++ - Code wie for (i = 0; i < LIMIT; i++)würde in Java übersetzt werden als for (i = 0; i < LIMIT; i = i + 1); weil es unangemessen wäre, das Atom zu verwenden i++. Was noch schlimmer ist, Programmierer, die von C oder anderen C-ähnlichen Sprachen zu Java kommen, würden i++sowieso verwenden, was zu einer unnötigen Verwendung atomarer Anweisungen führen würde.

Selbst auf der Ebene des Maschinenbefehlssatzes ist eine Inkrementoperation aus Leistungsgründen normalerweise nicht atomar. In x86 muss eine spezielle Anweisung "Sperrpräfix" verwendet werden, um die incAnweisung atomar zu machen : aus den gleichen Gründen wie oben. Wenn inces immer atomar wäre, würde es niemals verwendet werden, wenn eine nichtatomare Inc erforderlich ist; Programmierer und Compiler würden Code generieren, der lädt, 1 hinzufügt und speichert, weil er viel schneller wäre.

In einigen Befehlssatzarchitekturen gibt es kein Atom incoder vielleicht gar kein inc; Um eine atomare Inc auf MIPS auszuführen, müssen Sie eine Softwareschleife schreiben, die das llund sc: load-linked und store-bedingt verwendet. Load-linked liest das Wort und store-bedingt speichert den neuen Wert, wenn sich das Wort nicht geändert hat oder wenn es fehlschlägt (was erkannt wird und einen erneuten Versuch verursacht).

Kaz
quelle
2
Da Java keine Zeiger hat, ist das Inkrementieren lokaler Variablen von Natur aus threadsicher, sodass das Problem bei Schleifen meistens nicht so schlimm wäre. Ihr Punkt über die geringste Überraschung steht natürlich. auch, wie es ist, i = i + 1wäre eine Übersetzung für ++i, nichti++
Silly Freak
22
Das erste Wort der Frage ist "warum". Ab sofort ist dies die einzige Antwort auf das Problem "Warum". Die anderen Antworten geben die Frage wirklich nur noch einmal wieder. Also +1.
Dawood ibn Kareem
3
Es könnte erwähnenswert sein, dass eine Atomaritätsgarantie das Sichtbarkeitsproblem für Aktualisierungen von Nichtfeldern nicht lösen würde volatile. Wenn Sie also nicht jedes Feld als implizit behandeln, volatilesobald ein Thread den ++Operator verwendet hat, würde eine solche Atomaritätsgarantie keine gleichzeitigen Aktualisierungsprobleme lösen. Warum also möglicherweise Leistung für etwas verschwenden, wenn dies das Problem nicht löst?
Holger
1
@ DavidWallace meinst du nicht ++? ;)
Dan Hlavenka
36

i++ beinhaltet zwei Operationen:

  1. Lesen Sie den aktuellen Wert von i
  2. Erhöhen Sie den Wert und weisen Sie ihn zu i

Wenn zwei Threads i++gleichzeitig dieselbe Variable ausführen , erhalten beide möglicherweise denselben aktuellen Wert von iund erhöhen und setzen ihn dann auf i+1, sodass Sie anstelle von zwei eine einzelne Inkrementierung erhalten.

Beispiel:

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
quelle
(Selbst wenn i++ es atomar wäre, wäre es kein genau definiertes / thread-sicheres Verhalten.)
user2864740
15
+1, aber "1. A, 2. B und C" klingt nach drei Operationen, nicht nach zwei. :)
Yshavit
3
Beachten Sie, dass selbst wenn die Operation mit einer einzelnen Maschinenanweisung implementiert würde, die einen Speicherort inkrementiert, keine Garantie dafür besteht, dass sie threadsicher ist. Der Computer muss den Wert noch abrufen, inkrementieren und zurückspeichern. Außerdem können mehrere Cache-Kopien dieses Speicherorts vorhanden sein.
Hot Licks
3
@Aquarelle - Wenn zwei Prozessoren gleichzeitig denselben Vorgang für denselben Speicherort ausführen und am Standort keine "Reserve" -Sendung vorhanden ist, stören sie mit ziemlicher Sicherheit und führen zu falschen Ergebnissen. Ja, es ist möglich, dass dieser Vorgang "sicher" ist, aber es sind besondere Anstrengungen erforderlich, selbst auf Hardwareebene.
Hot Licks
6
Aber ich denke die Frage war "Warum" und nicht "Was passiert".
Sebastian Mach
11

Das Wichtigste ist die JLS (Java Language Specification) und nicht, wie verschiedene Implementierungen der JVM eine bestimmte Funktion der Sprache implementiert haben oder nicht. Das JLS definiert den ++ Postfix-Operator in Abschnitt 15.14.2, der ua besagt, dass "der Wert 1 zum Wert der Variablen addiert und die Summe wieder in der Variablen gespeichert wird". Nirgends wird Multithreading oder Atomizität erwähnt oder angedeutet. Für diese bietet das JLS flüchtige und synchronisierte . Zusätzlich gibt es das Paket java.util.concurrent.atomic (siehe http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html ).

Jonathan Rosenne
quelle
5

Warum ist i ++ in Java nicht atomar?

Teilen wir die Inkrementoperation in mehrere Anweisungen auf:

Thread 1 & 2:

  1. Wert der Summe aus dem Speicher abrufen
  2. Addiere 1 zum Wert
  3. Schreiben Sie zurück in den Speicher

Wenn es keine Synchronisation gibt, nehmen wir an, Thread 1 hat den Wert 3 gelesen und auf 4 erhöht, ihn aber nicht zurückgeschrieben. Zu diesem Zeitpunkt erfolgt der Kontextwechsel. Thread zwei liest den Wert 3, erhöht ihn und der Kontextwechsel erfolgt. Obwohl beide Threads den Gesamtwert erhöht haben, ist es immer noch eine 4-Race-Bedingung.

Aniket Thakur
quelle
2
Ich verstehe nicht, wie dies eine Antwort auf die Frage sein soll. Eine Sprache kann jedes Merkmal als atomar definieren, sei es Inkremente oder Einhörner. Sie veranschaulichen nur eine Konsequenz, wenn Sie nicht atomar sind.
Sebastian Mach
Ja, eine Sprache kann jedes Feature als atomar definieren, aber soweit Java als Inkrement-Operator betrachtet wird (dies ist die von OP gestellte Frage), ist es nicht atomar und meine Antwort gibt die Gründe an.
Aniket Thakur
1
(Entschuldigung für meinen harten Ton im ersten Kommentar) Aber dann scheint der Grund zu sein: "Wenn es atomar wäre, gäbe es keine Rennbedingungen." Das heißt, es klingt, als ob eine Rennbedingung wünschenswert ist.
Sebastian Mach
@phresnel Der Overhead, der eingeführt wird, um ein Inkrement atomar zu halten, ist riesig und selten erwünscht, wodurch die Operation billig gehalten wird und daher die meiste Zeit nicht atomar wünschenswert ist.
Josefx
4
@ Josefx: Beachten Sie, dass ich nicht die Fakten, sondern die Gründe in dieser Antwort in Frage stelle. Grundsätzlich heißt es: "i ++ ist in Java aufgrund der Rennbedingungen nicht atomar" , was so ist, als würde man sagen, "ein Auto hat keinen Airbag wegen der möglichen Unfälle" oder "Sie bekommen kein Messer mit Ihrer Currywurst-Bestellung, weil die wurst muss möglicherweise geschnitten werden " . Daher denke ich nicht, dass dies eine Antwort ist. Die Frage war nicht "Was macht i ++?" oder "Was ist die Folge davon, dass i ++ nicht synchronisiert wird?" .
Sebastian Mach
5

i++ ist eine Aussage, die einfach 3 Operationen beinhaltet:

  1. Aktuellen Wert lesen
  2. Schreibe einen neuen Wert
  3. Neuen Wert speichern

Diese drei Operationen sollen nicht in einem einzigen Schritt ausgeführt werden oder sind mit anderen Worten i++keine zusammengesetzte Operation. Infolgedessen können alle möglichen Probleme auftreten, wenn mehr als ein Thread an einer einzelnen, aber nicht zusammengesetzten Operation beteiligt ist.

Stellen Sie sich das folgende Szenario vor:

Zeit 1 :

Thread A fetches i
Thread B fetches i

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

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

Und da hast du es. Eine Rennbedingung.


Deshalb i++ist nicht atomar. Wenn es so wäre, wäre nichts davon passiert und jedes fetch-update-storewürde atomar passieren. Genau dafür AtomicIntegerist es gedacht und in Ihrem Fall würde es wahrscheinlich genau dazu passen.

PS

Ein ausgezeichnetes Buch, das all diese und noch einige weitere Themen behandelt: Java Concurrency in Practice

kstratis
quelle
1
Hmm. Eine Sprache kann jedes Merkmal als atomar definieren, sei es Inkremente oder Einhörner. Sie veranschaulichen nur eine Konsequenz, wenn Sie nicht atomar sind.
Sebastian Mach
@phresnel Genau. Ich weise aber auch darauf hin, dass es sich nicht um eine einzelne Operation handelt, was im weiteren Sinne impliziert, dass die Rechenkosten für die Umwandlung mehrerer solcher Operationen in atomare Operationen viel teurer sind, was wiederum teilweise rechtfertigt, warum i++nicht atomar ist.
Kstratis
1
Während ich Ihren Standpunkt verstehe, ist Ihre Antwort für das Lernen etwas verwirrend. Ich sehe ein Beispiel und eine Schlussfolgerung, die "wegen der Situation im Beispiel" sagt; imho das ist eine unvollständige Begründung :(
Sebastian Mach
1
@phresnel Vielleicht nicht die pädagogischste Antwort, aber es ist die beste, die ich derzeit anbieten kann. Hoffentlich hilft es den Menschen und verwirrt sie nicht. Vielen Dank für die Kritik. Ich werde versuchen, in meinen zukünftigen Beiträgen präziser zu sein.
Kstratis
2

In der JVM umfasst ein Inkrement ein Lesen und ein Schreiben, ist also nicht atomar.

celeritas
quelle
2

Wenn die Operation i++atomar wäre, hätten Sie nicht die Möglichkeit, den Wert daraus zu lesen. Dies ist genau das, was Sie verwenden möchten i++(anstatt zu verwenden ++i).

Schauen Sie sich zum Beispiel den folgenden Code an:

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

In diesem Fall erwarten wir folgende Ausgabe: 0 (weil wir ein Inkrement veröffentlichen, z. B. zuerst lesen, dann aktualisieren)

Dies ist einer der Gründe, warum die Operation nicht atomar sein kann, da Sie den Wert lesen müssen (und etwas damit tun müssen) und dann den Wert aktualisieren müssen.

Der andere wichtige Grund ist, dass das atomare Ausführen von etwas aufgrund des Sperren normalerweise mehr Zeit in Anspruch nimmt. Es wäre albern, wenn alle Operationen an Primitiven in den seltenen Fällen, in denen Menschen atomare Operationen durchführen möchten, etwas länger dauern würden. Deshalb haben sie der Sprache AtomicIntegerund andere Atomklassen hinzugefügt .

Roy van Rijn
quelle
2
Das ist irreführend. Sie müssen die Ausführung trennen und das Ergebnis erhalten, sonst könnten Sie keine Werte aus einer atomaren Operation erhalten.
Sebastian Mach
Nein, ist es nicht, deshalb hat Javas AtomicInteger ein get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () usw.
Roy van Rijn
1
Und die Java-Sprache hätte definiert werden können i++, um erweitert zu werden i.getAndIncrement(). Eine solche Erweiterung ist nicht neu. Beispielsweise werden Lambdas in C ++ zu anonymen Klassendefinitionen in C ++ erweitert.
Sebastian Mach
Wenn man ein Atom hat i++, kann man trivial ein Atom erzeugen ++ioder umgekehrt. Eins ist gleich dem anderen plus eins.
David Schwartz
2

Es gibt zwei Schritte:

  1. hol ich aus dem Speicher
  2. setze i + 1 auf i

Es ist also keine atomare Operation. Wenn thread1 i ++ ausführt und thread2 i ++ ausführt, kann der Endwert von i i + 1 sein.

Yanghaogn
quelle
-1

Parallelität (die ThreadKlasse und dergleichen) ist eine zusätzliche Funktion in Version 1.0 von Java .i++wurde zuvor in der Beta hinzugefügt, und als solche ist es immer noch mehr als wahrscheinlich in seiner (mehr oder weniger) ursprünglichen Implementierung.

Es ist Sache des Programmierers, Variablen zu synchronisieren. Lesen Sie dazu das Tutorial von Oracle .

Bearbeiten: Zur Verdeutlichung ist i ++ eine gut definierte Prozedur vor Java, und als solche haben die Entwickler von Java beschlossen, die ursprüngliche Funktionalität dieser Prozedur beizubehalten.

Der ++ - Operator wurde in B (1969) definiert, der nur ein bisschen älter ist als Java und Threading.

Die Fledermaus
quelle
-1 "public class Thread ... Seit: JDK1.0" Quelle: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak
Die Version ist weniger wichtig als die Tatsache, dass sie noch vor der Thread-Klasse implementiert wurde und deshalb nicht geändert wurde, aber ich habe meine Antwort bearbeitet, um Ihnen zu gefallen.
TheBat
5
Was zählt, ist, dass Ihre Behauptung "Es wurde noch vor der Thread-Klasse implementiert" nicht durch Quellen gestützt wird. i++Nicht atomar zu sein ist eine Entwurfsentscheidung, kein Versehen in einem wachsenden System.
Dumme Freak
Lol das ist süß. i ++ wurde lange vor Threads definiert, einfach weil es Sprachen gab, die vor Java existierten. Die Entwickler von Java verwendeten diese anderen Sprachen als Basis, anstatt ein gut akzeptiertes Verfahren neu zu definieren. Wo habe ich jemals gesagt, dass es ein Versehen ist?
TheBat
@SillyFreak Hier sind einige Quellen, die zeigen, wie alt ++ ist: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat