Ihr Idiom ist sicher , wenn und nur wenn der Verweis auf das HashMap
ist sicher veröffentlicht . Die sichere Veröffentlichung befasst sich nicht mit den Interna von sich HashMap
selbst, sondern damit, wie der Konstruktions-Thread den Verweis auf die Karte für andere Threads sichtbar macht.
Grundsätzlich besteht hier der einzig mögliche Wettlauf zwischen dem Aufbau des HashMap
und aller Lesethreads, die darauf zugreifen können, bevor er vollständig aufgebaut ist. In den meisten Diskussionen geht es darum, was mit dem Status des Kartenobjekts geschieht. Dies ist jedoch irrelevant, da Sie es nie ändern. Der einzig interessante Teil ist daher, wie die HashMap
Referenz veröffentlicht wird.
Stellen Sie sich zum Beispiel vor, Sie veröffentlichen die Karte folgendermaßen:
class SomeClass {
public static HashMap<Object, Object> MAP;
public synchronized static setMap(HashMap<Object, Object> m) {
MAP = m;
}
}
... und wird irgendwann setMap()
mit einer Karte aufgerufen, und andere Threads verwenden, SomeClass.MAP
um auf die Karte zuzugreifen, und suchen wie folgt nach Null:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Dies ist nicht sicher , obwohl es wahrscheinlich so aussieht, als ob es so wäre. Das Problem ist , dass es keine geschieht vor- Beziehung zwischen dem Satz SomeObject.MAP
und der nachfolgenden Leseoperation auf einem anderen Thread, so dass der Lesefaden frei , um zu sehen eine teilweise konstruierten Karte ist. Dies kann so ziemlich alles und sogar in der Praxis Dinge tun , wie den Lesethread in eine Endlosschleife zu bringen .
Um sicher die Karte zu veröffentlichen, müssen Sie etablieren ein geschieht zuvor Beziehung zwischen dem Schreiben des Referenz auf die HashMap
(dh der Veröffentlichung ) und die nachfolgenden Leser dieser Verweisung (dh der Verbrauch). Praktischerweise gibt es nur wenige leicht zu merkende Weise zu erreichen , dass [1] :
- Tauschen Sie die Referenz über ein ordnungsgemäß gesperrtes Feld aus ( JLS 17.4.5 ).
- Verwenden Sie den statischen Initialisierer, um die Speicher zu initialisieren ( JLS 12.4 ).
- Tauschen Sie die Referenz über ein flüchtiges Feld ( JLS 17.4.5 ) oder als Folge dieser Regel über die AtomicX-Klassen aus
- Initialisieren Sie den Wert in ein letztes Feld ( JLS 17.5 ).
Die für Ihr Szenario interessantesten sind (2), (3) und (4). Insbesondere gilt (3) direkt für den Code, den ich oben habe: Wenn Sie die Deklaration von MAP
in transformieren :
public static volatile HashMap<Object, Object> MAP;
dann ist alles koscher: Leser , die einen sehen Nicht-Null - Wert unbedingt eine zuvor passiert Beziehung mit dem Laden MAP
alle Geschäfte und damit mit der Karte Initialisierung zugeordnet sind .
Die anderen Methoden ändern die Semantik Ihrer Methode, da sowohl (2) (mit dem statischen Initializer) als auch (4) (mit final ) implizieren, dass Sie MAP
zur Laufzeit nicht dynamisch festlegen können . Wenn Sie dies nicht tun müssen , deklarieren Sie es einfach MAP
als static final HashMap<>
und Sie erhalten eine sichere Veröffentlichung.
In der Praxis sind die Regeln für den sicheren Zugriff auf "nie geänderte Objekte" einfach:
Wenn Sie ein Objekt veröffentlichen, das nicht von Natur aus unveränderlich ist (wie in allen deklarierten Feldern final
) und:
- Sie können bereits das Objekt erstellen, das zum Zeitpunkt der Deklaration zugewiesen wird. A : Verwenden Sie einfach ein
final
Feld (auch static final
für statische Elemente).
- Sie möchten das Objekt später zuordnen, nachdem die Referenz bereits sichtbar ist: ein flüchtiges Feld verwenden , b .
Das ist es!
In der Praxis ist es sehr effizient. Die Verwendung eines static final
Felds ermöglicht es der JVM beispielsweise, anzunehmen, dass der Wert für die Lebensdauer des Programms unverändert bleibt, und ihn stark zu optimieren. Die Verwendung eines final
Mitgliedes Feld ermöglicht die meisten Architekturen , das Feld auf eine Weise äquivalent zu einem normalen Lesefeld zu lesen und nicht hemmt weitere Optimierungen c .
Schließlich hat die Verwendung von volatile
einige Auswirkungen: Auf vielen Architekturen (z. B. x86, insbesondere solchen, bei denen Lesevorgänge keine Lesevorgänge zulassen) ist keine Hardwarebarriere erforderlich, aber einige Optimierungen und Neuordnungen treten möglicherweise beim Kompilieren nicht auf - dies jedoch Effekt ist im Allgemeinen gering. Im Gegenzug erhalten Sie tatsächlich mehr als gewünscht HashMap
- Sie können nicht nur eine sicher veröffentlichen , sondern auch so viele nicht geänderte HashMap
s speichern, wie Sie möchten, und sicher sein, dass alle Leser eine sicher veröffentlichte Karte sehen .
Weitere Informationen finden Sie in Shipilev oder in diesen FAQ von Manson und Goetz .
[1] Direkt aus dem Schiff zitieren .
a Das klingt kompliziert, aber ich meine, Sie können die Referenz zur Konstruktionszeit zuweisen - entweder am Deklarationspunkt oder im Konstruktor (Elementfelder) oder statischen Initialisierer (statische Felder).
b Optional können Sie eine synchronized
Methode zum Abrufen / Festlegen oder eine AtomicReference
oder etwas verwenden, aber wir sprechen über die Mindestarbeit, die Sie leisten können.
c Einige Architekturen mit sehr schwachen Speichermodellen (ich sehe Sie an , Alpha) erfordern möglicherweise eine Art Lesebarriere vor dem final
Lesen - aber diese sind heute sehr selten.