Lesen eines veralteten Werts nach dem Lesen eines neueren Werts [Duplikat]
Betrachten Sie dieses Beispiel. Hatten:
int var = 0;
Thread A:
System.out.println(var);
System.out.println(var);
Faden B:
var = 1;
Die Threads werden gleichzeitig ausgeführt. Ist folgende Ausgabe möglich?
1
0
Das heißt, der ursprüngliche Wert wird gelesen, nachdem der neue Wert gelesen wurde. Das var
ist nicht flüchtig. Mein Bauchgefühl ist, dass es nicht möglich ist.
Antworten
Sie verwenden System.out.println
das intern synchronized(this) {...}
, um die Dinge ein bisschen schlimmer zu machen. Aber selbst damit kann Ihr Leser-Thread immer noch beobachten 1, 0
, dh: eine rassige Lektüre.
Ich bin bei weitem kein Experte dafür, aber nachdem ich viele Videos / Beispiele / Blogs von Alexey Shipilev durchgesehen habe, denke ich, dass ich zumindest etwas verstehe.
JLS gibt an, dass:
Wenn x und y Aktionen desselben Threads sind und x in Programmreihenfolge vor y steht, dann hb (x, y).
Da beide Lesevorgänge von in var
sind program order
, können wir zeichnen:
(po)
firstRead(var) ------> secondRead(var)
// po == program order
Dieser Satz sagt auch, dass dies eine happens-before
Ordnung aufbaut , also:
(hb)
firstRead(var) ------> secondRead(var)
// hb == happens before
Aber das ist innerhalb "des gleichen Threads". Wenn wir über mehrere Threads nachdenken möchten, müssen wir die Synchronisationsreihenfolge untersuchen . Wir brauchen das, weil der gleiche Absatz über happens-before order
sagt:
Wenn eine Aktion x mit einer folgenden Aktion y synchronisiert wird , haben wir auch hb (x, y).
Wenn wir also diese Aktionskette zwischen program order
und aufbauen synchronizes-with order
, können wir über das Ergebnis nachdenken. Wenden wir das auf Ihren Code an:
(NO SW) (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)
// NO SW == there is "no synchronizes-with order" here
// hb == happens-before
Und hier happens-before consistency
kommt es im selben Kapitel ins Spiel :
Eine Menge von Aktionen A findet statt - bevor sie konsistent sind, wenn für alle Lesevorgänge r in A, wobei W (r) die von r gesehene Schreibaktion ist, weder hb (r, W (r)) noch die dort ist existiert ein Schreiben w in A, so dass wv = rv und hb (W (r), w) und hb (w, r).
In einer konsistenten Reihe von Aktionen, die vor einer konsistenten Gruppe von Aktionen ausgeführt werden, wird bei jedem Lesevorgang ein Schreibvorgang angezeigt, den die Aktion vor der Bestellung sehen kann
Ich gebe zu, dass ich den ersten Satz sehr vage verstehe und hier hat Alexey mir am meisten geholfen, wie er es ausdrückt:
Lesevorgänge sehen entweder den letzten Schreibvorgang, der beim
happens-before
oder einem anderen Schreibvorgang stattgefunden hat .
Da es dort kein synchronizes-with order
und implizit kein gibt, happens-before order
darf der Lesethread über ein Rennen lesen. und damit bekommen 1
, als 0
.
Sobald Sie eine korrekte synchronizes-with order
, zum Beispiel eine von hier, einführen
Eine Entsperraktion auf dem Monitor m synchronisiert sich mit allen nachfolgenden Sperraktionen auf ...
Ein Schreibvorgang in eine flüchtige Variable v wird synchronisiert - mit allen nachfolgenden Lesevorgängen von v durch einen beliebigen Thread ...
Das Diagramm ändert sich (sagen wir, Sie haben sich dafür entschieden var
volatile
):
SW PO
write(var) ---------> firstRead(var) -------> secondRead(var)
// SW == there IS "synchronizes-with order" here
// PO == happens-before
PO
(Programmreihenfolge) gibt dies HB
(passiert vorher) über den ersten Satz an, den ich in dieser Antwort aus dem JLS zitiert habe. Und SW
gibt HB
weil:
Wenn eine Aktion x mit einer folgenden Aktion y synchronisiert wird, haben wir auch hb (x, y).
So wie:
HB HB
write(var) ---------> firstRead(var) -------> secondRead(var)
Und jetzt happens-before order
heißt es, dass der Lesethread den Wert liest, der "in der letzten HB geschrieben" wurde, oder dass dies bedeutet, dass das Lesen 1
dann 0
unmöglich ist.
Ich habe das Beispiel von jcstress genommen und eine kleine Änderung eingeführt (genau wie bei Ihnen System.out.println
):
@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {
private final Holder h1 = new Holder();
private final Holder h2 = h1;
private static class Holder {
int a;
int trap;
}
@Actor
public void actor1() {
h1.a = 1;
}
@Actor
public void actor2(II_Result r) {
Holder h1 = this.h1;
Holder h2 = this.h2;
h1.trap = 0;
h2.trap = 0;
synchronized (this) {
r.r1 = h1.a;
}
synchronized (this) {
r.r2 = h2.a;
}
}
}
Beachten Sie, synchronized(this){....}
dass dies nicht Teil des ursprünglichen Beispiels ist. Selbst bei der Synchronisation kann ich das immer noch 1, 0
als Ergebnis sehen. Dies ist nur um zu beweisen, dass auch mit synchronized
(das kommt intern von System.out.println
), Sie immer noch 1
als bekommen können 0
.
Wenn der Wert von var
gelesen wird und 1
sich nicht ändert, ändert er sich nicht zurück. Diese Ausgabe kann weder aufgrund von Sichtbarkeit noch aufgrund von Neuordnungen erfolgen. Was passieren kann ist 0 0
, 0 1
und 1 1
.
Der wichtigste Punkt, den Sie hier verstehen sollten, ist println
die Synchronisation. Schauen Sie in diese Methode und Sie sollten synchronized
dort eine sehen. Diese Blöcke haben den Effekt, dass die Drucke genau in dieser Reihenfolge ausgeführt werden. Während das Schreiben jederzeit erfolgen kann, ist es nicht möglich, dass der erste Druck den neuen Wert von var
sieht, der zweite Druck jedoch den alten Wert. Daher kann das Schreiben nur vor beiden Ausdrucken, zwischen oder nach ihnen erfolgen.
Außerdem gibt es keine Garantie dafür, dass der Schreibvorgang überhaupt sichtbar ist, da er var
weder mit markiert ist volatile
noch in irgendeiner Weise synchronisiert ist.
Ich denke, was hier fehlt, ist die Tatsache, dass diese Threads auf tatsächlichen physischen Kernen laufen und wir hier nur wenige mögliche Varianten haben:
Alle Threads laufen auf demselben Kern, dann wird das Problem auf die Ausführungsreihenfolge dieser 3 Befehle reduziert. In diesem Fall ist 1,0 nicht möglich. Ich denke, Druckausführungen werden aufgrund der durch die Synchronisation erzeugten Speicherbarrieren geordnet schließt 1,0 aus
A und B laufen auf 2 verschiedenen Kernen, dann scheint 1,0 auch nicht möglich zu sein, sobald der Kern, der Thread A ausführt, 1 liest, gibt es keine Möglichkeit, 0 danach zu lesen, wie oben angegeben.
Thread A wird zwischen diesen beiden Drucken neu geplant, sodass der zweite Druck auf einem anderen Kern ausgeführt wird, entweder derselbe wie B ausgeführt wurde / wird oder auf einem anderen dritten Kern. Wenn also die 2 Drucke auf verschiedenen Kernen ausgeführt werden, hängt es davon ab, welchen Wert 2 Kerne sehen. Wenn var nicht synchronisiert ist (es ist nicht klar, ob var ein Mitglied davon ist), können diese 2 Kerne unterschiedliche var-Werte sehen Es besteht die Möglichkeit für 1,0.
Dies ist also ein Cache-Kohärenzproblem.
PS Ich bin kein JVM-Experte, daher könnten hier andere Dinge im Spiel sein.
Hinzufügen zu den anderen Antworten:
Mit long
und double
sind Schreibvorgänge möglicherweise nicht atomar, sodass die ersten 32 Bits vor den letzten 32 Bits sichtbar werden können, oder umgekehrt. Daher können völlig unterschiedliche Werte ausgegeben werden.