Wofür werden in Java Speicherzäune verwendet?

18

Beim Versuch zu verstehen, wie SubmissionPublisher( Quellcode in Java SE 10, OpenJDK | docs ), eine neue Klasse, die in Version 9 zu Java SE hinzugefügt wurde, implementiert wurde, stieß ich auf einige API-Aufrufe, die VarHandlemir bisher nicht bekannt waren:

fullFence, acquireFence, releaseFence, loadLoadFenceUnd storeStoreFence.

Nachdem ich einige Nachforschungen angestellt habe, insbesondere in Bezug auf das Konzept der Gedächtnisbarrieren / -zäune (ich habe bereits von ihnen gehört, ja; aber ich habe sie nie benutzt und war daher mit ihrer Semantik nicht vertraut), denke ich, dass ich ein grundlegendes Verständnis dafür habe, wofür sie sind . Da sich meine Fragen jedoch aus einem Missverständnis ergeben könnten, möchte ich sicherstellen, dass ich sie überhaupt richtig verstanden habe:

  1. Speicherbarrieren ordnen Einschränkungen in Bezug auf Lese- und Schreibvorgänge neu.

  2. Speicherbarrieren können in zwei Hauptkategorien eingeteilt werden: unidirektionale und bidirektionale Speicherbarrieren, je nachdem, ob sie Einschränkungen für Lese- oder Schreibvorgänge oder für beide festlegen.

  3. C ++ unterstützt eine Vielzahl von Speicherbarrieren , die jedoch nicht mit den von bereitgestellten übereinstimmen VarHandle. Einige der in verfügbaren Speicherbarrieren VarHandlebieten jedoch Reihenfolgeeffekte , die mit den entsprechenden C ++ - Speicherbarrieren kompatibel sind .

    • #fullFence ist kompatibel mit atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence ist kompatibel mit atomic_thread_fence(memory_order_acquire)
    • #releaseFence ist kompatibel mit atomic_thread_fence(memory_order_release)
    • #loadLoadFenceund #storeStoreFencehaben kein kompatibles C ++ - Gegenstück

Das Wort kompatibel scheint hier wirklich wichtig zu sein, da sich die Semantik in den Details deutlich unterscheidet. Beispielsweise sind alle C ++ - Barrieren bidirektional, während dies bei Java nicht (unbedingt) der Fall ist.

  1. Die meisten Speicherbarrieren haben auch Synchronisationseffekte. Diese hängen insbesondere vom verwendeten Barrieretyp und den zuvor ausgeführten Barriereanweisungen in anderen Threads ab. Da die vollständigen Auswirkungen eines Barriereanweises hardwarespezifisch sind, bleibe ich bei den übergeordneten Barrieren (C ++). In C ++, zum Beispiel Änderungen vor einer Freigabesperrbefehl an einen Faden sichtbar sind Ausführung eines acquire Sperrbefehls.

Sind meine Annahmen richtig? Wenn ja, sind meine daraus resultierenden Fragen:

  1. VarHandleVerursachen die verfügbaren Speicherbarrieren irgendeine Art von Speichersynchronisation?

  2. Unabhängig davon, ob sie eine Speichersynchronisation verursachen oder nicht, wofür können Einschränkungen bei der Neuordnung in Java nützlich sein? Das Java-Speichermodell bietet bereits einige sehr starke Garantien hinsichtlich der Reihenfolge, wenn flüchtige Felder, Sperren oder VarHandleOperationen wie diese #compareAndSetbetroffen sind.

Für den Fall, dass Sie nach einem Beispiel suchen: Die oben genannte BufferedSubscriptioninnere Klasse von SubmissionPublisher(Quelle oben verlinkt) hat in Zeile 1079 einen vollständigen Zaun eingerichtet (Funktion growAndAdd; da die verlinkte Website keine Fragment-IDs unterstützt, nur STRG + F) ). Es ist mir jedoch unklar, wofür es da ist.

Quaffel
quelle
1
Ich habe versucht zu antworten, aber um es ganz einfach auszudrücken, sie existieren, weil die Leute einen schwächeren Modus wollen als Java. In aufsteigender Reihenfolge wären dies : plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Antworten:

11

Dies ist eigentlich hauptsächlich eine Nichtantwort (wollte ursprünglich einen Kommentar abgeben, aber wie Sie sehen, ist es viel zu lang). Es ist nur so, dass ich das selbst viel hinterfragt, viel gelesen und recherchiert habe und zu diesem Zeitpunkt kann ich mit Sicherheit sagen: Das ist kompliziert. Ich habe sogar mehrere Tests mit jcstress geschrieben, um herauszufinden, wie sie wirklich funktionieren (während ich mir den generierten Assembler-Code anschaue), und obwohl einige davon irgendwie Sinn machten, ist das Thema im Allgemeinen keineswegs einfach.

Das allererste, was Sie verstehen müssen:

In der Java Language Specification (JLS) werden nirgendwo Hindernisse erwähnt . Für Java wäre dies ein Implementierungsdetail: Es handelt sich tatsächlich um Ereignisse vor der Semantik. Um diese gemäß dem JMM (Java Memory Model) richtig spezifizieren zu können, müsste sich das JMM ziemlich stark ändern .

Dies ist in Arbeit.

Zweitens, wenn Sie hier wirklich die Oberfläche kratzen möchten, ist dies das allererste, was Sie sehen sollten . Das Gespräch ist unglaublich. Mein Lieblingsteil ist, wenn Herb Sutter seine 5 Finger hebt und sagt: "So viele Menschen können wirklich und richtig damit arbeiten." Das sollte Ihnen einen Hinweis auf die Komplexität geben. Trotzdem gibt es einige triviale Beispiele, die leicht zu verstehen sind (wie ein Zähler, der von mehreren Threads aktualisiert wird und sich nicht um andere Speichergarantien kümmert, sondern nur darum , dass er selbst korrekt inkrementiert wird).

Ein anderes Beispiel ist, wenn (in Java) Sie möchten, dass ein volatileFlag das Stoppen / Starten von Threads steuert. Sie wissen, die klassische:

volatile boolean stop = false; // on thread writes, one thread reads this    

Wenn Sie mit Java arbeiten, wissen Sie, dass ohne volatile diesen Code ein Fehler vorliegt (Sie können beispielsweise nachlesen, warum die doppelte Überprüfung der Sperre ohne diesen Code fehlerhaft ist). Aber wissen Sie auch, dass dies für einige Leute, die Hochleistungscode schreiben, zu viel ist? volatileLesen / Schreiben garantiert auch sequentielle Konsistenz - das hat einige starke Garantien und einige Leute wollen eine schwächere Version davon.

Eine thread-sichere Flagge, aber nicht flüchtig? Ja genau : VarHandle::set/getOpaque.

Und Sie würden sich fragen, warum jemand das zum Beispiel braucht? Nicht jeder interessiert sich für all die Änderungen, die von a huckepack genommen werden volatile.

Mal sehen, wie wir dies in Java erreichen. Erstens gab es solche exotischen Dinge bereits in der API : AtomicInteger::lazySet. Dies ist im Java-Speichermodell nicht spezifiziert und hat keine klare Definition . immer noch benutzten die Leute es (LMAX, afaik oder dies zum Lesen ). IMHO AtomicInteger::lazySetist VarHandle::releaseFence(oder VarHandle::storeStoreFence).


Versuchen wir zu beantworten, warum jemand diese braucht .

JMM bietet grundsätzlich zwei Möglichkeiten, auf ein Feld zuzugreifen: einfach und flüchtig (was eine sequentielle Konsistenz garantiert ). Alle diese Methoden, die Sie erwähnen, sind dazu da, etwas zwischen diese beiden zu bringen - Semantik freigeben / erwerben ; Es gibt Fälle, in denen die Leute das tatsächlich brauchen.

Eine noch größere Entspannung von Release / Acquisition wäre undurchsichtig , was ich immer noch zu verstehen versuche .


Fazit (Ihr Verständnis ist übrigens ziemlich korrekt): Wenn Sie dies in Java verwenden möchten - sie haben derzeit keine Spezifikation -, tun Sie dies auf eigenes Risiko. Wenn Sie sie verstehen möchten, sind ihre C ++ - äquivalenten Modi der Ausgangspunkt.

Eugene
quelle
1
Versuchen Sie nicht, die Bedeutung von lazySetdurch Verknüpfung mit alten Antworten herauszufinden. Die aktuelle Dokumentation sagt genau, was es heutzutage bedeutet. Außerdem ist es irreführend zu sagen, dass das JMM nur zwei Zugriffsmodi hat. Wir haben flüchtiges Lesen und flüchtiges Schreiben , was zusammen eine Beziehung vor dem Geschehen herstellen kann.
Holger
1
Ich war gerade dabei, etwas mehr darüber zu schreiben. Bedenken Sie, dass cas sowohl ein Lesen als auch ein Schreiben ist und sich wie eine vollständige Barriere verhält, und Sie werden vielleicht verstehen, warum es erwünscht ist, es zu entspannen. Wenn Sie beispielsweise eine Sperre implementieren, lautet die erste Aktion cas (0, 1) für die Anzahl der Sperren, Sie müssen jedoch nur die Semantik (wie das flüchtige Lesen) erfassen, während das endgültige Schreiben von 0 zum Entsperren eine Freigabesemantik (wie das flüchtige Schreiben) haben sollte ), so gibt es einen Fall zuvor zwischen Entriegelung und anschließender Verriegelung. Acquire / Release ist in Bezug auf Threads mit unterschiedlichen Sperren noch schwächer als Volatile Read / Write.
Holger
1
@Peter Cordes: Die erste C-Version mit einem volatileSchlüsselwort war C99, fünf Jahre nach Java, aber es fehlte immer noch eine nützliche Semantik, selbst C ++ 03 hat kein Speichermodell. Die Dinge, die C ++ "atomar" nennt, sind auch viel jünger als Java. Und das volatileSchlüsselwort impliziert nicht einmal atomare Aktualisierungen. Warum sollte es so heißen?
Holger
1
@PeterCordes vielleicht verwechsle ich es mit restrict, aber ich erinnere mich an Zeiten, als ich schreiben musste __volatile, um eine Compiler-Erweiterung ohne Schlüsselwörter zu verwenden. Vielleicht hat es C89 nicht vollständig implementiert? Sag mir nicht, dass ich so alt bin . Vor Java 5 volatilewar es viel näher an C. Aber Java hatte kein MMIO, daher war sein Zweck immer Multithreading, aber die Semantik vor Java 5 war dafür nicht sehr nützlich. Release / Acquisition wie Semantik wurde hinzugefügt, aber es ist nicht atomar (atomare Updates sind eine zusätzliche Funktion, die darauf aufgebaut ist).
Holger
2
@Eugene diesbezüglich war mein Beispiel spezifisch für die Verwendung von cas zum Sperren, das erworben werden würde. Ein Countdown-Latch würde atomare Dekremente mit Release-Semantik tragen, gefolgt von dem Thread, der Null erreicht, einen Erfassungszaun einfügt und die letzte Aktion ausführt. Natürlich gibt es auch andere Fälle für atomare Aktualisierungen, in denen der vollständige Zaun erforderlich bleibt.
Holger