wake-up-neo.com

Ist es sicher, Werte aus einer Java.util.HashMap von mehreren Threads zu erhalten (keine Änderung)?

Es gibt einen Fall, in dem eine Karte erstellt wird. Sobald sie initialisiert ist, wird sie nie mehr geändert. Es wird jedoch (nur über get (key)) von mehreren Threads aus zugegriffen. Ist es sicher, einen Java.util.HashMap auf diese Weise zu verwenden?

(Derzeit verwende ich gerne einen Java.util.concurrent.ConcurrentHashMap und habe kein gemessenes Bedürfnis, die Leistung zu verbessern, bin aber einfach neugierig, ob eine einfache HashMap ausreichen würde. Daher ist diese Frage nicht "Welche sollte ich verwenden?" Oder auch nicht Ist es eine Frage der Performance? Die Frage lautet eher: "Wäre es sicher?"

118
Dave L.

Ihre Redewendung ist genau dann sicher , wenn der Verweis auf das HashMapsicher veröffentlicht ​​ist. Anstatt die Interna von HashMap selbst in Beziehung zu setzen, befasst sich safe publication damit, wie der Konstruktions-Thread den Verweis auf die Map für andere Threads sichtbar macht.

Grundsätzlich besteht der einzig mögliche Wettlauf zwischen der Konstruktion des HashMap und jeglichen Lesethreads, die darauf zugreifen können, bevor es vollständig konstruiert ist. Die meiste Diskussion dreht sich darum, was mit dem Zustand des Kartenobjekts passiert. Dies ist jedoch irrelevant, da Sie es nie ändern. Der einzig interessante Teil ist also, wie die Referenz HashMap veröffentlicht wird.

Stellen Sie sich zum Beispiel vor, Sie veröffentlichen die Map folgendermaßen:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... und irgendwann wird setMap() mit einer Map aufgerufen, und andere Threads verwenden SomeClass.MAP, um auf die Map zuzugreifen.

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 ist. Das Problem ist, dass es keine happen-before Beziehung zwischen der Menge von SomeObject.MAP Und dem anschließenden Lesen in einem anderen Thread gibt Das Lesen eines Threads ist kostenlos, um eine teilweise aufgebaute Karte zu sehen. Dies kann so ziemlich alles tun irgendetwas und sogar in der Praxis macht es Dinge wie , die den Lesethread in eine Endlosschleife setzen .

Um die Karte sicher zu veröffentlichen, müssen Sie eine happen-before Beziehung zwischen dem Schreiben der Referenz zum HashMap (dh dem Publikation) und die nachfolgenden Leser dieser Referenz (dh den Verbrauch). Praktischerweise gibt es nur ein paar leicht zu merkende Möglichkeiten, dies zu erreichen [1]:

  1. Tauschen Sie die Referenz durch ein ordnungsgemäß gesperrtes Feld aus ( JLS 17.4.5 )
  2. Verwenden Sie den statischen Initialisierer, um die Initialisierungsspeicher auszuführen ( JLS 12.4 )
  3. Tauschen Sie die Referenz über ein flüchtiges Feld ( JLS 17.4.5 ) oder als Folge dieser Regel über die AtomicX-Klassen aus
  4. 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:

public static volatile HashMap<Object, Object> MAP;

dann ist alles koscher: Leser, die einen non-null -Wert sehen, haben notwendigerweise eine happen-before -Beziehung zum Geschäft zu MAP und sehen daher alle Geschäfte im Zusammenhang mit der Karteninitialisierung.

Die anderen Methoden ändern die Semantik Ihrer Methode, da sowohl (2) (unter Verwendung des statischen Initalisierers) als auch (4) (unter Verwendung von final) implizieren, dass Sie MAP zur Laufzeit nicht dynamisch setzen können . Wenn Sie dies nicht tun müssen, deklarieren Sie einfach MAP als static final HashMap<> Und Sie sind garantiert sicher veröffentlicht.

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 nveränderlich ist (wie in allen mit final deklarierten Feldern) und:

  • Sie können bereits das Objekt erstellen, das zum Zeitpunkt der Deklaration zugewiesen wirdein: Verwenden Sie einfach ein final -Feld (einschließlich static final für statische Elemente).
  • Sie möchten das Objekt später zuweisen, nachdem die Referenz bereits sichtbar ist: Verwenden Sie ein flüchtiges Feldb.

Das ist es!

In der Praxis ist es sehr effizient. Durch die Verwendung eines static final - Feldes kann die JVM beispielsweise davon ausgehen, dass der Wert für die Laufzeit des Programms unverändert bleibt, und ihn stark optimieren. Die Verwendung eines final -Elementfelds ermöglicht es most ​​- Architekturen, das Feld auf eine Weise zu lesen, die einem normalen Feldlesevorgang entspricht, und verhindert keine weiteren Optimierungenc.

Schließlich hat die Verwendung von volatile einige Auswirkungen: Auf vielen Architekturen (z. B. x86) ist keine Hardwarebarriere erforderlich, insbesondere auf solchen, bei denen Lesevorgänge nicht zugelassen sind. Einige Optimierungs- und Neuordnungsvorgänge können jedoch nicht durchgeführt werden zur Kompilierungszeit - aber dieser Effekt ist im Allgemeinen gering. Im Gegenzug erhalten Sie tatsächlich mehr als das, wonach Sie gefragt haben - Sie können nicht nur sicher ein HashMap veröffentlichen, sondern auch so viele nicht geänderte HashMaps speichern, wie Sie möchten Seien Sie versichert, dass alle Leser eine sicher veröffentlichte Karte sehen.

Weitere Einzelheiten finden Sie in Shipilev oder this FAQ von Manson und Goetz .


[1] Direktes Zitieren von shipilev .


ein Das hört sich kompliziert an, aber ich meine, dass Sie die Referenz zur Konstruktionszeit zuweisen können - entweder am Deklarationspunkt oder im Konstruktor (Elementfelder) oder im statischen Initialisierer (statische Felder).

b Optional können Sie eine synchronized -Methode verwenden, um/set abzurufen, oder eine AtomicReference oder etwas anderes, aber wir sprechen über die minimale Arbeit, die Sie tun können.

c Einige Architekturen mit sehr schwachen Speichermodellen (ich schaue auf Sie, Alpha) erfordern möglicherweise eine Art Lesesperre vor einem final -Lesevorgang - aber diese sind heute sehr selten.

42
BeeOnRope

Jeremy Manson, der Gott, wenn es um das Java Memory Model geht, hat einen dreiteiligen Blog zu diesem Thema - denn im Wesentlichen stellen Sie die Frage "Ist der Zugriff auf eine unveränderliche HashMap sicher" - die Antwort ist "Ja". Aber Sie müssen das Prädikat auf die Frage beantworten, die lautet: "Ist meine HashMap unveränderlich". Die Antwort könnte Sie überraschen - Java hat relativ komplizierte Regeln, um die Unveränderlichkeit zu bestimmen.

Weitere Informationen zum Thema finden Sie in den Blogbeiträgen von Jeremy:

Teil 1 zur Unveränderlichkeit in Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-Java.html

Teil 2 zu Unveränderlichkeit in Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-2.html

Teil 3 zu Unveränderlichkeit in Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-3.html

70
Taylor Gautier

Die Lesevorgänge sind vom Standpunkt der Synchronisation aus sicher, jedoch nicht vom Standpunkt des Speichers. Dies ist etwas, das unter Java-Entwicklern, einschließlich hier bei Stackoverflow, weitgehend missverstanden wird. (Beachten Sie die Bewertung von diese Antwort zum Beweis.)

Wenn andere Threads ausgeführt werden, wird möglicherweise keine aktualisierte Kopie der HashMap angezeigt, wenn kein Arbeitsspeicher aus dem aktuellen Thread geschrieben wird. Speicherschreibvorgänge werden durch die Verwendung synchronisierter oder flüchtiger Schlüsselwörter oder durch Verwendung einiger Java-Parallelitätskonstrukte verursacht.

Siehe Artikel von Brian Goetz über das neue Java-Speichermodell für Details. 

34
Heath Borders

Nach etwas genauerem Nachschauen fand ich dies in der Java doc (Hervorhebung meiner):

Beachten Sie, dass diese Implementierung nicht .__ ist. synchronisiert. Wenn mehrere Threads Greifen Sie gleichzeitig auf eine Hash-Map zu und um Mindestens einer der Threads ändert die Map strukturell muss es sein extern synchronisiert. (Eine strukturelle Modifikation ist jede Operation, die Eine oder mehrere Mappings hinzufügt oder löscht; Lediglich den Wert ändert Mit einem Schlüssel, den eine Instanz Bereits enthält, ist keine strukturelle Änderung.)

Dies scheint zu implizieren, dass es sicher sein wird, vorausgesetzt, das Gegenteil der Aussage ist wahr.

10
Dave L.

Ein Hinweis ist, dass ein get () von einer nicht synchronisierten HashMap unter Umständen eine Endlosschleife verursachen kann. Dies kann vorkommen, wenn ein gleichzeitiges put () die Karte erneut aufbereitet.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

9
Alex Miller

Es gibt jedoch eine wichtige Wendung. Der Zugriff auf die Map ist sicher, aber im Allgemeinen kann nicht garantiert werden, dass alle Threads genau denselben Status (und damit Werte) der HashMap sehen. Dies kann auf Multiprozessorsystemen der Fall sein, bei denen die Änderungen an der HashMap, die von einem Thread (z. B. dem, der ihn gefüllt hat) vorgenommen werden, im Cache dieser CPU liegen können und von Threads, die auf anderen CPUs ausgeführt werden, nicht sichtbar sind, bis eine Memory-Fence-Operation durchgeführt wird durchgeführt, um die Cache-Kohärenz sicherzustellen. Die Java-Sprachspezifikation ist explizit: Die Lösung besteht darin, eine Sperre zu erhalten (synchronisiert (...)), die eine Memory-Fence-Operation auslöst. Wenn Sie also sicher sind, dass nach dem Auffüllen der HashMap jeder Thread eine ANY-Sperre erhält, ist es ab diesem Zeitpunkt in Ordnung, von jedem Thread auf die HashMap zuzugreifen, bis die HashMap erneut geändert wird.

8
Alexander

Gemäß http://www.ibm.com/developerworks/Java/library/j-jtp03304/ # Initialisierungssicherheit können Sie Ihre HashMap zu einem abschließenden Feld machen. Wenn der Konstruktor fertig ist, wird er sicher veröffentlicht.

Unter dem neuen Speichermodell gibt es eine ähnliche Beziehung wie eine zufällige Beziehung zwischen dem Schreiben eines letzten Felds in einem Konstruktor und dem anfänglichen Laden einer gemeinsam genutzten Referenz auf dieses Objekt in einem anderen Thread. ...

4
bodrin

In dem von Ihnen beschriebenen Szenario müssen Sie also eine Menge Daten in eine Map einfügen. Wenn Sie mit dem Auffüllen fertig sind, behandeln Sie sie als unveränderlich. Ein Ansatz, der "sicher" ist (dh Sie erzwingen, dass er wirklich als unveränderlich behandelt wird), besteht darin, die Referenz durch Collections.unmodifiableMap(originalMap) zu ersetzen, wenn Sie bereit sind, sie unveränderlich zu machen.

Ein Beispiel dafür, wie schlecht Karten bei gleichzeitiger Verwendung fehlschlagen können, und die vorgeschlagene Umgehung, die ich erwähnt habe, finden Sie in diesem Bug-Parade-Eintrag: bug_id = 6423457

3
Will

Seien Sie gewarnt, dass das Ersetzen einer ConcurrentHashMap durch eine HashMap auch in Single-Threaded-Code möglicherweise nicht sicher ist. ConcurrentHashMap verbietet null als Schlüssel oder Wert. HashMap verbietet sie nicht (nicht fragen).

In der unwahrscheinlichen Situation, dass Ihr bestehender Code während des Setups der Sammlung möglicherweise eine Null hinzufügt (vermutlich in einem Fehlerfall), ändert das Ersetzen der Sammlung wie beschrieben das Funktionsverhalten.

Das heißt, vorausgesetzt, Sie tun nichts anderes, als dass gleichzeitige Lesevorgänge aus einer HashMap sicher sind. 

[Bearbeiten: Mit "gleichzeitige Lesevorgänge" meine ich, dass es keine gleichzeitigen Änderungen gibt.

Andere Antworten erläutern, wie dies sichergestellt werden kann. Eine Möglichkeit besteht darin, die Karte unveränderlich zu machen, dies ist jedoch nicht erforderlich. Beispielsweise definiert das Speichermodell JSR133 explizit das Starten eines Threads als synchronisierte Aktion. Das bedeutet, dass Änderungen, die in Thread A vor dem Start von Thread B vorgenommen wurden, in Thread B sichtbar sind.

Ich möchte den ausführlicheren Antworten zum Java-Speichermodell nicht widersprechen. Diese Antwort soll darauf hinweisen, dass es abgesehen von Parallelitätsproblemen mindestens einen API-Unterschied zwischen ConcurrentHashMap und HashMap gibt, der sogar ein Singlethread-Programm, das eines durch das andere ersetzt, untergraben könnte.

1
Steve Jessop

Wenn die Initialisierung und jeder Put synchronisiert sind, sind Sie in Sicherheit.

Folgender Code ist sicher, da der Classloader für die Synchronisation sorgt:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

Der folgende Code ist sicher, da das Schreiben von Volatile für die Synchronisation sorgt.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

Dies funktioniert auch, wenn die Elementvariable final ist, da final ebenfalls flüchtig ist. Und wenn die Methode ein Konstruktor ist.

0
TomWolk

http://www.docjar.com/html/api/Java/util/HashMap.Java.html

hier ist die Quelle für HashMap. Wie Sie sehen, gibt es absolut keinen Sperr-/Mutex-Code.

Dies bedeutet, dass es in Ordnung ist, in einer Multithread-Situation aus einer HashMap zu lesen, ich jedoch auf jeden Fall eine ConcurrentHashMap verwenden würde, wenn es mehrere Schreibvorgänge gab.

Interessant ist, dass sowohl .NET HashTable als auch Dictionary <K, V> Synchronisationscode eingebaut haben.

0
FlySwat