Lesen eines veralteten Werts nach dem Lesen eines neueren Werts [Duplikat]

Nov 24 2020

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 varist nicht flüchtig. Mein Bauchgefühl ist, dass es nicht möglich ist.

Antworten

3 Eugene Nov 26 2020 at 11:41

Sie verwenden System.out.printlndas 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 varsind program order, können wir zeichnen:

                (po) 
firstRead(var) ------> secondRead(var)
// po == program order

Dieser Satz sagt auch, dass dies eine happens-beforeOrdnung 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 ordersagt:

Wenn eine Aktion x mit einer folgenden Aktion y synchronisiert wird , haben wir auch hb (x, y).

Wenn wir also diese Aktionskette zwischen program orderund 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 consistencykommt 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-beforeoder einem anderen Schreibvorgang stattgefunden hat .

Da es dort kein synchronizes-with orderund implizit kein gibt, happens-before orderdarf 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 SWgibt HBweil:

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 orderheißt es, dass der Lesethread den Wert liest, der "in der letzten HB geschrieben" wurde, oder dass dies bedeutet, dass das Lesen 1dann 0unmö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, 0als Ergebnis sehen. Dies ist nur um zu beweisen, dass auch mit synchronized(das kommt intern von System.out.println), Sie immer noch 1als bekommen können 0.

1 akuzminykh Nov 24 2020 at 17:04

Wenn der Wert von vargelesen wird und 1sich 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 1und 1 1.

Der wichtigste Punkt, den Sie hier verstehen sollten, ist printlndie Synchronisation. Schauen Sie in diese Methode und Sie sollten synchronizeddort 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 varsieht, 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 varweder mit markiert ist volatilenoch in irgendeiner Weise synchronisiert ist.

AlexRevetchi Nov 26 2020 at 15:32

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:

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

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

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

FrancescoMenzani Nov 25 2020 at 01:35

Hinzufügen zu den anderen Antworten:

Mit longund doublesind 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.