Wie tief schwankende Veröffentlichung garantiert?

Wie wir wissen, garantieren wir, dass, wenn wir eine Objektreferenz haben und diese Referenz das letzte Feld hat – wir sehen alle erreichbaren Felder vom letzten Feld (zumindest wenn der Konstruktor fertig war)

Beispiel 1:

class Foo{ private final Map map; Foo(){ map = new HashMap(); map.put(1,"object"); } public void bar(){ System.out.println(map.get(1)); } } 

Wie ich in diesem Fall erkläre, haben wir die Garantie, dass die bar() Methode immer ein object ausgibt object weil:
1. Ich habe den vollständigen Code der class Foo aufgelistet und die Karte ist endgültig;
2. Wenn ein Thread den Verweis auf Foo und diesen Verweis! = Null sehen wird, haben wir Garantien, dass der vom letzten map erreichbare Wert tatsächlich ist .

Das denke ich auch

Beispiel 2:

 class Foo { private final Map map; private Map nonFinalMap; Foo() { nonFinalMap = new HashMap(); nonFinalMap.put(2, "ololo"); map = new HashMap(); map.put(1, "object"); } public void bar() { System.out.println(map.get(1)); } public void bar2() { System.out.println(nonFinalMap.get(2)); } } 

Hier haben wir die gleichen Garantien für bar() -Methode, aber bar2 kann NullPointerException obwohl nonFinalMap Zuweisung von nonFinalMap Zuweisung der map .

Ich möchte wissen wie flüchtig:

Beispiel 3:

 class Foo{ private volatile Map map; Foo(){ map = new HashMap(); map.put(1,"object"); } public void bar(){ System.out.println(map.get(1)); } } 

Wie ich verstehe, bar() -Methode kann nicht NullPoinerException casting, aber es kann null drucken; (Ich bin mir über diesen Aspekt nicht ganz sicher)

Beispiel 4:

 class Foo { private volatile Map map; private Map nonVolatileMap; Foo() { nonVolatileMap= new HashMap(); nonVolatileMap.put(2, "ololo"); map = new HashMap(); map.put(1, "object"); } public void bar() { System.out.println(map.get(1)); } public void bar2() { System.out.println(nonFinalMap.get(2)); } } 

Ich denke, hier haben wir gleiche Garantien über bar() Methode auch bar2() kann nicht casting NullPointerException weil nonVolatileMap Zuordnung geschrieben höher flüchtige Zuordnung nonVolatileMap aber es kann null ausgeben


Hinzugefügt nach Elliott Frisch Kommentar

Veröffentlichung durch Rennbeispiel:

 public class Main { private static Foo foo; public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { foo = new Foo(); } }).start(); new Thread(new Runnable() { @Override public void run() { while (foo == null) ; // empty loop foo.bar(); } }).start(); } } 

Bitte überprüfen oder korrigieren Sie meine Kommentare zu Code-Schnipsel.

Im Bereich des aktuellen Java-Speichermodells ist volatile nicht gleichbedeutend. Mit anderen Worten, Sie können das final nicht durch volatile ersetzen und denken, dass die Garantien für die sichere Konstruktion gleich sind. Bemerkenswerterweise kann dies theoretisch passieren:

 public class M { volatile int x; M(int v) { this.x = v; } int x() { return x; } } // thread 1 m = new M(42); // thread 2 M lm; while ((lm = m) == null); // wait for it print(lm.x()); // allowed to print "0" 

Also ist das Schreiben des volatile Feldes im Konstruktor nicht so sicher.

Intuition: Im obigen Beispiel gibt es eine Rasse auf m . Diese Rasse wird nicht dadurch eliminiert, dass das Feld Mx volatile , nur dass das m selbst volatile wäre. Mit anderen Worten, der volatile Modifikator in diesem Beispiel ist am falschen Ort , um nützlich zu sein. Bei der sicheren Veröffentlichung müssen Sie “schreiben -> flüchtiges Schreiben -> flüchtiges Lesen, das flüchtiges Schreiben beobachtet -> Lesen (jetzt beobachten schreibt vor dem flüchtigen Schreiben)”, und stattdessen haben Sie “flüchtiges Schreiben -> schreiben -> lesen – > flüchtiges Lesen (das nicht den flüchtigen Schreibvorgang beobachtet) “.

Trivia 1: Diese Eigenschaft bedeutet, dass wir volatile -s in Konstruktoren viel aggressiver optimieren können. Dies bestätigt die Intuition, dass unbeobachtetes flüchtiges Material (und tatsächlich wird es nicht beobachtet werden kann, bis Konstruktor mit Nicht-Entweichen fertig ist) entspannt werden kann.

Trivia 2: Dies bedeutet auch, dass Sie volatile Variablen nicht sicher initialisieren können. Ersetzen AtomicInteger im obigen Beispiel M durch AtomicInteger , und Sie haben ein eigenartiges Verhalten im realen Leben! Rufen Sie den new AtomicInteger(42) in einem Thread auf, veröffentlichen Sie die Instanz unsicher und führen Sie get() in einem anderen Thread aus – werden Sie garantiert 42 beobachten? JMM sagt, wie gesagt, “nein”. Neuere Überarbeitungen des Java-Speichermodells versuchen, eine sichere Konstruktion für alle Initialisierungen zu gewährleisten, um diesen Fall zu erfassen. Und viele Nicht-x86-Ports, auf die es ankommt, haben dies bereits verstärkt , um sicher zu sein.

Trivia 3: Doug Lea : “Dieses final vs. volatile Problem hat zu einigen twisty Konstruktionen in java.util.concurrent geführt, um 0 als Basis / Default-Wert in Fällen zuzulassen, in denen es nicht natürlich wäre. Diese Regel ist stinkend und sollte geändert werden. ”

Das heißt, das Beispiel kann schlauer gemacht werden:

 public class C { int v; C(int v) { this.x = v; } int x() { return x; } } public class M { volatile C c; M(int v) { this.c = new C(v); } int x() { while (c == null); // wait! return cx(); } } // thread 1 m = new M(42); // thread 2 M lm; while ((lm = m) == null); // wait for it print(lm.x()); // always prints "42" 

Wenn nach flüchtigem Lesen ein transitives Lesen durch volatile Feld beobachtet wird, beobachtet der Wert, der durch flüchtiges Schreiben in Konstruktor geschrieben wird, die üblichen sicheren Veröffentlichungsregeln.