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 VarHandle
mir bisher nicht bekannt waren:
fullFence
, acquireFence
, releaseFence
, loadLoadFence
Und 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:
Speicherbarrieren ordnen Einschränkungen in Bezug auf Lese- und Schreibvorgänge neu.
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.
C ++ unterstützt eine Vielzahl von Speicherbarrieren , die jedoch nicht mit den von bereitgestellten übereinstimmen
VarHandle
. Einige der in verfügbaren SpeicherbarrierenVarHandle
bieten jedoch Reihenfolgeeffekte , die mit den entsprechenden C ++ - Speicherbarrieren kompatibel sind .#fullFence
ist kompatibel mitatomic_thread_fence(memory_order_seq_cst)
#acquireFence
ist kompatibel mitatomic_thread_fence(memory_order_acquire)
#releaseFence
ist kompatibel mitatomic_thread_fence(memory_order_release)
#loadLoadFence
und#storeStoreFence
haben 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.
- 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:
VarHandle
Verursachen die verfügbaren Speicherbarrieren irgendeine Art von Speichersynchronisation?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
VarHandle
Operationen wie diese#compareAndSet
betroffen sind.
Für den Fall, dass Sie nach einem Beispiel suchen: Die oben genannte BufferedSubscription
innere 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.
plain -> opaque -> release/acquire -> volatile (sequential consistency)
.Antworten:
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:
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
volatile
Flag das Stoppen / Starten von Threads steuert. Sie wissen, die klassische: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?volatile
Lesen / Schreiben garantiert auch sequentielle Konsistenz - das hat einige starke Garantien und einige Leute wollen eine schwächere Version davon.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 ). IMHOAtomicInteger::lazySet
istVarHandle::releaseFence
(oderVarHandle::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.
quelle
lazySet
durch 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.volatile
Schlü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 dasvolatile
Schlüsselwort impliziert nicht einmal atomare Aktualisierungen. Warum sollte es so heißen?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 5volatile
war 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).