Warum gewährleistet flüchtig in Java 5+ nicht die Sichtbarkeit von einem anderen Thread?

68

Gemäß:

http://www.ibm.com/developerworks/library/j-jtp03304/

Wenn unter dem neuen Speichermodell Thread A in eine flüchtige Variable V schreibt und Thread B aus V liest, ist garantiert, dass alle Variablenwerte, die zum Zeitpunkt des Schreibens von V für A sichtbar waren, jetzt für B sichtbar sind

Und viele Stellen im Internet geben an, dass der folgende Code niemals "Fehler" ausgeben sollte:

public class Test {
    volatile static private int a;
    static private int b;

    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (a==0) {

                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }

        b = 1;
        a = 1;
    }
}

b sollte für alle Threads a1 sein, wenn 1 ist.

Allerdings bekomme ich manchmal "Fehler" gedruckt . Wie ist das möglich?

Oleg
quelle
1
@OliCharlesworth Ich denke, er fragt, warum die verschiedenen zwischengespeicherten Werte von nach dem flüchtigen Schreiben / Lesen bnicht synchronisiert werden . b=1a
Yshavit
1
Haben Sie diesen Code tatsächlich ausgeführt und "Fehler" mit Java 1.5+ gedruckt gesehen?
Assylias
4
@OfekRon Nach dem Java-Speichermodell spielt es keine Rolle, ob bes flüchtig ist oder nicht, da auf das Schreiben in ein flüchtiges var ein Schreiben folgt und im anderen Thread dem Lesen desselben das Lesen desselben vorausgeht flüchtige var.
Marko Topolnik
17
Dieser Thread wird derzeit
diskutiert
4
Nur ein kurzes Update von der Liste der Parallelitätsinteressen. Es sieht so aus, als ob dies in der neuesten Version von Java7 behoben wurde: download.java.net/jdk7u6/changes/jdk7u6-b14.html (Überprüfen Sie den letzten Eintrag im Hotspot-Bereich. Der Fehler ID-Links zu einem Fehlerbericht mit Ihrem Anwendungsfall.
Yshavit

Antworten:

34

Aktualisieren:

Für alle Interessierten wurde dieser Fehler behoben und für Java 7u6 Build B14 behoben. Sie können den Fehlerbericht / die Fehlerbehebungen hier sehen

Ursprüngliche Antwort

Wenn Sie in Bezug auf die Sichtbarkeit / Reihenfolge des Gedächtnisses denken, müssen Sie über die Beziehung nachdenken, die vor dem Ereignis besteht. Die wichtige Voraussetzung für b != 0ist für a == 1. Wenn a != 1dann kann b entweder 0 oder 1 sein.

Sobald ein Thread sieht, a == 1wird dieser Thread garantiert sehen b == 1.

Post Java 5, im OP-Beispiel, sobald der while(a == 0)Ausbruch b garantiert 1 ist

Bearbeiten:

Ich habe die Simulation viele Male ausgeführt und Ihre Ausgabe nicht gesehen.

Unter welchem ​​Betriebssystem, welcher Java-Version und welcher CPU testen Sie?

Ich bin unter Windows 7, Java 1.6_24 (versuche es mit _31)

Bearbeiten 2:

Ein großes Lob an das OP und Walter Laan - Für mich geschah dies nur, als ich von 64-Bit-Java auf 32-Bit-Java auf einem 64-Bit-Windows 7 umstieg (aber nicht ausgeschlossen werden darf).

Edit 3:

Die Zuordnung zu ttoder vielmehr das Statikget von bscheint einen signifikanten Einfluss zu haben (um dies zu beweisen, entfernen Sie dasint tt = b; und es sollte immer funktionieren.

Es scheint, dass das Laden von bin ttdas Feld lokal speichert, das dann im if-Koniditon verwendet wird (der Verweis auf diesen Wert nicht tt). Wenn dies b == 0zutrifft, bedeutet dies wahrscheinlich, dass der lokale Speicher tt0 war (zu diesem Zeitpunkt ist es ein Rennen , dem lokalen Speicher 1 zuzuweisen tt). Dies scheint nur für 32-Bit-Java 1.6 & 7 mit festgelegtem Client zu gelten.

Ich habe die beiden Ausgangsbaugruppen verglichen und der unmittelbare Unterschied war hier. (Denken Sie daran, dies sind Ausschnitte).

Dieser gedruckte "Fehler"

 0x021dd753: test   %eax,0x180100      ;   {poll}
  0x021dd759: cmp    $0x0,%ecx
  0x021dd75c: je     0x021dd748         ;*ifeq
                                        ; - Test$1::run@7 (line 13)
  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::run@13 (line 17)
  0x021dd767: nop    
  0x021dd768: jmp    0x021dd7b8         ;   {no_reloc}
  0x021dd76d: xchg   %ax,%ax
  0x021dd770: jmp    0x021dd7d2         ; implicit exception: dispatches to 0x021dd7c2
  0x021dd775: nop                       ;*getstatic out
                                        ; - Test$1::run@16 (line 18)
  0x021dd776: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x021dd7dc
  0x021dd778: mov    $0x39239500,%edx   ;*invokevirtual println

Und

Dies druckte nicht "Fehler"

0x0226d763: test   %eax,0x180100      ;   {poll}
  0x0226d769: cmp    $0x0,%edx
  0x0226d76c: je     0x0226d758         ;*ifeq
                                        ; - Test$1::run@7 (line 13)
  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access$0@0 (line 3)
                                        ; - Test$1::run@10 (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::run@13 (line 17)
  0x0226d782: nopw   0x0(%eax,%eax,1)
  0x0226d788: jmp    0x0226d7ed         ;   {no_reloc}
  0x0226d78d: xchg   %ax,%ax
  0x0226d790: jmp    0x0226d807         ; implicit exception: dispatches to 0x0226d7f7
  0x0226d795: nop                       ;*getstatic out
                                        ; - Test$1::run@16 (line 18)
  0x0226d796: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x0226d811
  0x0226d798: mov    $0x39239500,%edx   ;*invokevirtual println

In diesem Beispiel stammt der erste Eintrag aus einem Lauf, der "Fehler" druckte, während der zweite aus einem Lauf stammte, der dies nicht tat.

Es scheint, dass der Arbeitslauf bvor dem Testen korrekt geladen und zugewiesen wurde und gleich 0 ist.

  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access$0@0 (line 3)
                                        ; - Test$1::run@10 (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::run@13 (line 17)

Während des Laufs, der "Fehler" druckte, wurde die zwischengespeicherte Version von geladen %edx

  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::run@13 (line 17)

Für diejenigen, die mehr Erfahrung mit Assembler haben, wiegen Sie bitte :)

Bearbeiten 4

Sollte meine letzte Bearbeitung sein, da die Parallelitätsentwickler eine Hand darauf bekommen, habe ich mit und ohne die int tt = b;Aufgabe noch etwas mehr getestet . Ich fand heraus, dass wenn ich das Maximum von 100 auf 1000 erhöhe, es eine 100% ige Fehlerrate zu geben scheint, wenn int tt = bes eingeschlossen ist, und eine 0% ige Chance, wenn es ausgeschlossen ist.

John Vint
quelle
Aber das OP sagt, dass dies nicht das Verhalten ist, das er beobachtet.
Oliver Charlesworth
@OliCharlesworth Dann muss er sich auf einer Java-Laufzeit befinden, die nicht dem Java 5-Speichermodell entspricht, oder etwas falsch machen. Ich werde dies selbst testen und sehen, ob ich die gleiche Interaktion beobachte. Ich habe das Gefühl, dass ich es nicht
tun
1
Entfernt es die b == 0 als JIT-Optimierung?
John Vint
2
Ich habe -XX: + UnlockDiagnosticVMOptions -XX: + PrintCompilation -XX: + PrintAssembly verwendet, das vom Eclipse-Debug (aber auch bei normaler Ausführung) mit JDK6u30 32-Bit (auf einem 64-Bit-Computer) ausgeführt wird.
Walter Laan
2
Ich wette, es ist ein OSR-Fehler, es ist nicht der erste OSR-Fehler (fav. Inkl. TieredCompilation w / c1-> c2 und ein JVM-Absturz).
Bests
12

Basierend auf dem folgenden Auszug aus JCiP hätte ich gedacht, dass Ihr Beispiel niemals "Fehler" drucken sollte:

Die Sichtbarkeitseffekte flüchtiger Variablen gehen über den Wert der flüchtigen Variablen selbst hinaus. Wenn ein Thread A in eine flüchtige Variable schreibt und anschließend Thread B dieselbe Variable liest, werden die Werte aller Variablen, die vor dem Schreiben in die flüchtige Variable für A sichtbar waren, nach dem Lesen der flüchtigen Variablen für B sichtbar .

Assylien
quelle
Ich würde dies +1, weil ich denke, dass wir niemals "Fehler" sehen könnten ... außer dass einige Leute berichten, dass sie "Fehler" sehen, also muss dies irgendwie nicht zutreffen!
Yshavit
Für die Leute, die diesen Fehler nie sehen ... nur um die Dinge einzugrenzen ... auf welcher JVM und CPU testen Sie?
CHao
2
@ JohnVint Hölle ja. Damit reproduziert -d32es zuverlässig das Problem.
Marko Topolnik
1
@MarkoTopolnik Wenn man sich die Assembly ansieht, scheint es (obwohl ich mich irren kann), dass sie, wenn sie fehlschlägt, auf den lokalen Speicher von verweist tt. In dem 'b' == 0 ist wahr.
John Vint
1
@ JohnVint Ich bin in [Parallelitätsinteresse] auf diesen Thread gestoßen, es ist nur ein 32-Bit-Client, der Probleme verursacht. Ich habe auf meinem Computer überprüft, mit -server sehe ich den Effekt nicht.
Marko Topolnik
-2

Meiner Meinung nach ist das Problem aufgrund mangelnder Synchronisation aufgetreten :

BEACHTEN : Wenn b = 1 vor a = 1 auftritt und a flüchtig ist, während b nicht ist, wird b = 1 tatsächlich erst nach Abschluss von a = 1 für alle Threads aktualisiert (gemäß der Logik des Quate).

Was in Ihrem Code steht, ist, dass b = 1 zuerst nur für den Hauptprozess aktualisiert wurde, dann erst, wenn die flüchtige Zuweisung abgeschlossen ist, alle Threads b aktualisiert wurden. Ich denke, dass Zuweisungen von flüchtigen Stoffen möglicherweise nicht als atomare Operationen funktionieren (muss weit zeigen und den Rest der Referenzen irgendwie aktualisieren, um sich wie flüchtige Stoffe zu verhalten), daher wäre dies meine Vermutung, warum ein Thread b = 0 anstelle von b = 1 liest.

Betrachten Sie diese Änderung des Codes, die meine Behauptung zeigt:

public class Test {
    volatile static private int a;
    static private int b;
    private static Object lock = new Object();


    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (true) {
                        synchronized (lock ) {
                            if (a!=0) break;
                         }
                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }
        b = 1;
        synchronized (lock ) {
        a = 1;
        }  
    }
}
Ofek Ron
quelle
4
Leider bist du falsch. Es gibt spezielle Anforderungen. Schauen Sie sich g.oswego.edu/dl/jmm/cookbook.html an . Sie werden in diesem Can ReorderRaster feststellen, welche Synchronisation das JMM verspricht. Die wichtigen Teile sind: 1. Normaler Speicher kann nicht mit einem nachfolgenden flüchtigen Speicher neu angeordnet werden, und 2. Flüchtige Ladung kann nicht mit einem nachfolgenden normalen Laden neu angeordnet werden. Beide dieses Beispiel erklärt
John Vint