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();
}
}
java
multithreading
concurrency
Andie2302
quelle
quelle
AtomicInteger
?iinc
Operation 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.iinc
Anweisung für Felder gab, garantiert eine einzelne Anweisung keine Atomizität, z. B. Nicht-volatile
long
und Feldzugriff istdouble
nicht garantiert atomar, unabhängig davon, dass sie von einer einzelnen Bytecode-Anweisung ausgeführt wird.Antworten:
i++
ist in Java wahrscheinlich nicht atomar, da Atomizität eine spezielle Anforderung ist, die in den meisten Anwendungen von nicht vorhanden isti++
. 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 wirdi = 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 alsfor (i = 0; i < LIMIT; i = i + 1)
; weil es unangemessen wäre, das Atom zu verwendeni++
. Was noch schlimmer ist, Programmierer, die von C oder anderen C-ähnlichen Sprachen zu Java kommen, würdeni++
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
inc
Anweisung atomar zu machen : aus den gleichen Gründen wie oben. Wenninc
es 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
inc
oder vielleicht gar keininc
; Um eine atomare Inc auf MIPS auszuführen, müssen Sie eine Softwareschleife schreiben, die dasll
undsc
: 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).quelle
i = i + 1
wäre eine Übersetzung für++i
, nichti++
volatile
. Wenn Sie also nicht jedes Feld als implizit behandeln,volatile
sobald 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?++
? ;)i++
beinhaltet zwei Operationen:i
i
Wenn zwei Threads
i++
gleichzeitig dieselbe Variable ausführen , erhalten beide möglicherweise denselben aktuellen Wert voni
und erhöhen und setzen ihn dann aufi+1
, sodass Sie anstelle von zwei eine einzelne Inkrementierung erhalten.Beispiel:
quelle
i++
es atomar wäre, wäre es kein genau definiertes / thread-sicheres Verhalten.)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 ).
quelle
Teilen wir die Inkrementoperation in mehrere Anweisungen auf:
Thread 1 & 2:
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.
quelle
i++
ist eine Aussage, die einfach 3 Operationen beinhaltet: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 :
Zeit 2 :
Zeit 3 :
Und da hast du es. Eine Rennbedingung.
Deshalb
i++
ist nicht atomar. Wenn es so wäre, wäre nichts davon passiert und jedesfetch-update-store
würde atomar passieren. Genau dafürAtomicInteger
ist 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
quelle
i++
nicht atomar ist.In der JVM umfasst ein Inkrement ein Lesen und ein Schreiben, ist also nicht atomar.
quelle
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öchteni++
(anstatt zu verwenden++i
).Schauen Sie sich zum Beispiel den folgenden Code an:
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
AtomicInteger
und andere Atomklassen hinzugefügt .quelle
i++
, um erweitert zu werdeni.getAndIncrement()
. Eine solche Erweiterung ist nicht neu. Beispielsweise werden Lambdas in C ++ zu anonymen Klassendefinitionen in C ++ erweitert.i++
, kann man trivial ein Atom erzeugen++i
oder umgekehrt. Eins ist gleich dem anderen plus eins.Es gibt zwei Schritte:
Es ist also keine atomare Operation. Wenn thread1 i ++ ausführt und thread2 i ++ ausführt, kann der Endwert von i i + 1 sein.
quelle
Parallelität (die
Thread
Klasse 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.
quelle
i++
Nicht atomar zu sein ist eine Entwurfsentscheidung, kein Versehen in einem wachsenden System.