Data Races und sequentielle Konsistenzgarantien
Data Races entstehen, wenn Programme nicht korrekt synchronisiert werden. Die Java-Speichermodellspezifikation definiert den Datenwettbewerb wie folgt:
Schreiben in eine Variable in einem Thread,
Lesen derselben Variablen in einem anderen Thread,
und das Schreiben und Lesen sind nicht durch Synchronisierung geordnet.
Wenn Code Datenrennen enthält, führt die Programmausführung oft zu kontraintuitiven Ergebnissen (wie es im Beispiel des vorherigen Kapitels der Fall war). Wenn ein Multithread-Programm korrekt synchronisiert ist, ist das Programm ein Programm ohne Datenrennen.
JMM bietet die folgenden Garantien für die Speicherkonsistenz korrekt synchronisierter Multithread-Programme:
Wenn das Programm korrekt synchronisiert ist, ist die Ausführung des Programms sequentiell konsistent – das heißt, die Ausführung Das Ergebnis ist dasselbe, als ob das Programm in einem sequenziell konsistenten Speichermodell ausgeführt würde (wie wir gleich sehen werden, ist dies eine äußerst starke Garantie für den Programmierer). Synchronisation bezieht sich hier auf Synchronisation im weitesten Sinne, einschließlich der korrekten Verwendung gemeinsamer Synchronisationsprimitive (Lock, Volatile und Final).
Sequential Consistency Memory Model
Das Sequential Consistency Memory Model ist ein theoretisches Referenzmodell, das von Informatikern idealisiert wurde und Programmierern starke Garantien für die Sichtbarkeit des Speichers bietet. Das sequentielle Konsistenzspeichermodell weist zwei Hauptmerkmale auf:
Alle Vorgänge in einem Thread müssen in der Reihenfolge des Programms ausgeführt werden.
(Unabhängig davon, ob das Programm synchronisiert ist oder nicht) Alle Threads können nur eine einzige Reihenfolge der Operationsausführung sehen. In einem sequentiell konsistenten Speichermodell muss jede Operation atomar ausgeführt werden und für alle Threads sofort sichtbar sein.
Das sequentielle Konsistenzmodell bietet dem Programmierer die folgende Ansicht:
Konzeptionell verfügt das sequentielle Konsistenzmodell über einen einzigen globalen Speicher. Dies kann der Speicher sein über einen nach links und rechts schwenkbaren Schalter mit jedem Thread verbunden. Gleichzeitig muss jeder Thread Speicher-Lese-/Schreibvorgänge in der Reihenfolge des Programms ausführen. Aus der obigen Abbildung können wir ersehen, dass zu jedem Zeitpunkt höchstens ein Thread mit dem Speicher verbunden sein kann. Wenn mehrere Threads gleichzeitig ausgeführt werden, kann das Umschaltgerät in der Abbildung alle Speicher-Lese-/Schreibvorgänge aller Threads serialisieren.
Zum besseren Verständnis verwenden wir im Folgenden zwei schematische Diagramme, um die Eigenschaften des sequentiellen Konsistenzmodells näher zu erläutern.
Angenommen, es gibt zwei Threads A und B, die gleichzeitig ausgeführt werden. Thread A hat drei Operationen und ihre Reihenfolge im Programm ist: A1->A2->A3. Thread B verfügt ebenfalls über drei Operationen, und ihre Reihenfolge im Programm ist: B1->B2->B3.
Angenommen, die beiden Threads verwenden Monitore, um korrekt zu synchronisieren: Thread A gibt den Monitor frei, nachdem drei Vorgänge ausgeführt wurden, und Thread B erhält anschließend denselben Monitor. Dann ist der Ausführungseffekt des Programms im sequentiellen Konsistenzmodell wie in der folgenden Abbildung dargestellt:
Nehmen wir nun an, dass die beiden Threads nicht synchronisiert sind. Hier ist der unsynchronisiertes Programm Schematische Darstellung der Ausführung im sequentiellen Konsistenzmodell:
Unsynchronisiertes Programm Im sequentiellen Konsistenzmodell ist die Gesamtausführungsreihenfolge zwar ungeordnet, alle Threads können jedoch nur a sehen konsistente Gesamtausführungssequenz. Am Beispiel der obigen Abbildung lautet die Ausführungsreihenfolge der Threads A und B: B1->A1->A2->B2->A3->B3. Diese Garantie wird erreicht, weil jede Operation in einem sequentiell konsistenten Speichermodell für jeden Thread sofort sichtbar sein muss.
Allerdings gibt es bei JMM keine solche Garantie. Nicht nur ist die Gesamtausführungsreihenfolge eines nicht synchronisierten Programms in JMM nicht in der richtigen Reihenfolge, sondern auch die Reihenfolge der Operationsausführung, die von allen Threads gesehen wird, kann inkonsistent sein. Bevor der aktuelle Thread beispielsweise die geschriebenen Daten im lokalen Speicher zwischenspeichert und sie nicht im Hauptspeicher aktualisiert, ist der Schreibvorgang nur für den aktuellen Thread aus der Perspektive anderer Threads sichtbar Der Vorgang wurde vom aktuellen Thread überhaupt nicht ausgeführt. Erst nachdem der aktuelle Thread die in den lokalen Speicher geschriebenen Daten in den Hauptspeicher geleert hat, kann dieser Schreibvorgang für andere Threads sichtbar sein. In diesem Fall ist die Reihenfolge, in der Vorgänge ausgeführt werden, zwischen dem aktuellen Thread und anderen Threads inkonsistent.
Sequentieller Konsistenzeffekt synchronisierter Programme
Nachfolgend verwenden wir den Monitor, um das vorherige Beispielprogramm ReorderExample zu synchronisieren, um zu sehen, wie ein korrekt synchronisiertes Programm sequentielle Konsistenz aufweist.
Bitte schauen Sie sich den folgenden Beispielcode an:
class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a; …… } } }
Im obigen Beispielcode wird davon ausgegangen, dass Thread B die Methode „reader()“ ausführt, nachdem Thread A die Methode „writer()“ ausgeführt hat. Verfahren. Dies ist ein ordnungsgemäß synchronisiertes Multithread-Programm. Gemäß der JMM-Spezifikation sind die Ausführungsergebnisse dieses Programms dieselben wie die Ausführungsergebnisse dieses Programms im sequentiellen Konsistenzmodell. Das Folgende ist eine Vergleichstabelle des Ausführungszeitpunkts des Programms in den beiden Speichermodellen:
Im sequentiellen Konsistenzmodell werden alle Operationen seriell in der Reihenfolge des Programms ausgeführt. In JMM kann der Code im kritischen Abschnitt neu angeordnet werden (JMM lässt jedoch nicht zu, dass der Code im kritischen Abschnitt außerhalb des kritischen Abschnitts „entweicht“, was die Semantik des Monitors zerstören würde). JMM führt zu den beiden Schlüsselzeitpunkten des Verlassens des Monitors und des Betretens des Monitors eine spezielle Verarbeitung durch, sodass der Thread zu diesen beiden Zeitpunkten dieselbe Speicheransicht wie das sequentielle Konsistenzmodell hat (spezifische Details werden später erläutert). Obwohl Thread A im kritischen Abschnitt eine Neuordnung vorgenommen hat, kann Thread B aufgrund der sich gegenseitig ausschließenden Ausführungseigenschaften des Monitors hier die Neuordnung von Thread A im kritischen Abschnitt nicht „beobachten“. Diese Neuordnung verbessert die Ausführungseffizienz, ohne die Ausführungsergebnisse des Programms zu ändern.
Von hier aus können wir die Grundpolitik von JMM in der spezifischen Implementierung erkennen: Ohne die (korrekt synchronisierten) Ergebnisse der Programmausführung zu ändern, öffnen Sie so viele Annehmlichkeiten wie möglich für die Compiler- und Prozessoroptimierung.
Ausführungsmerkmale nicht synchronisierter Programme
Für Multithread-Programme, die nicht synchronisiert oder falsch synchronisiert sind, bietet JMM nur minimale Sicherheit: Der bei der Ausführung des Threads gelesene Wert ist entweder der Wert eines vorherigen Der von jedem Thread geschriebene Wert ist entweder der Standardwert (0, null, falsch). JMM stellt sicher, dass der von der Thread-Leseoperation gelesene Wert nicht aus dem Nichts erscheint. Um eine minimale Sicherheit zu erreichen, löscht die JVM beim Zuweisen eines Objekts auf dem Heap zunächst den Speicherplatz und weist dann das Objekt darauf zu (die JVM synchronisiert diese beiden Vorgänge intern). Daher ist die Standardinitialisierung der Domäne bereits abgeschlossen, wenn dem Objekt vorab auf Null gesetzter Speicher zugewiesen wird.
JMM garantiert nicht, dass das Ausführungsergebnis eines nicht synchronisierten Programms mit dem Ausführungsergebnis des Programms im sequentiellen Konsistenzmodell übereinstimmt. Denn wenn ein nicht synchronisiertes Programm im sequentiellen Konsistenzmodell ausgeführt wird, ist es im Allgemeinen außer Betrieb und seine Ausführungsergebnisse sind unvorhersehbar. Es macht keinen Sinn, sicherzustellen, dass unsynchronisierte Programme in beiden Modellen mit konsistenten Ergebnissen ausgeführt werden.
Ähnlich wie beim sequentiellen Konsistenzmodell ist ein nicht synchronisiertes Programm bei der Ausführung in JMM im Allgemeinen außer Betrieb und seine Ausführungsergebnisse sind unvorhersehbar. Gleichzeitig weisen die Ausführungseigenschaften unsynchronisierter Programme in diesen beiden Modellen die folgenden Unterschiede auf:
Das sequentielle Konsistenzmodell garantiert, dass Vorgänge innerhalb eines einzelnen Threads in der Reihenfolge des Programms ausgeführt werden, während JMM dies nicht garantiert Vorgänge innerhalb eines einzelnen Threads werden in der Reihenfolge des Programms ausgeführt (z. B. die obige Neuordnung des korrekt synchronisierten Multithread-Programms innerhalb des kritischen Abschnitts). Dies wurde bereits besprochen und wird hier nicht wiederholt.
Das sequentielle Konsistenzmodell garantiert, dass alle Threads nur eine konsistente Reihenfolge der Operationsausführung sehen können, während JMM nicht garantiert, dass alle Threads eine konsistente Reihenfolge der Operationsausführung sehen können. Dies wurde bereits erwähnt und wird hier nicht wiederholt.
JMM garantiert keine Atomizität für Lese-/Schreibvorgänge für 64-Bit-Long- und Double-Variablen, während das sequentielle Konsistenzmodell Atomizität für alle Lese-/Schreibvorgänge im Speicher garantiert.
Der dritte Unterschied hängt eng mit dem Arbeitsmechanismus des Prozessorbusses zusammen. In einem Computer werden Daten über einen Bus zwischen Prozessor und Speicher übertragen. Jede Datenübertragung zwischen Prozessor und Speicher erfolgt über eine Reihe von Schritten, die als Bustransaktion bezeichnet werden. Zu den Bustransaktionen gehören Lesetransaktionen und Schreibtransaktionen. Lesetransaktionen übertragen Daten vom Speicher zum Prozessor und Schreibtransaktionen übertragen Daten vom Prozessor zum Speicher. Jede Transaktion liest/schreibt ein oder mehrere physisch aufeinanderfolgende Wörter im Speicher. Der Schlüssel hier ist, dass der Bus Transaktionen synchronisiert, die gleichzeitig versuchen, den Bus zu nutzen. Während ein Prozessor eine Bustransaktion ausführt, verhindert der Bus, dass alle anderen Prozessoren und E/A-Geräte Speicher lesen/schreiben. Lassen Sie uns den Funktionsmechanismus des Busses anhand eines schematischen Diagramms veranschaulichen:
Wie in der Abbildung oben gezeigt, wird davon ausgegangen, dass die Prozessoren A, B und C gleichzeitig Bustransaktionen zum Bus initiieren. Zu diesem Zeitpunkt wird die Bus-Arbitrierung über den Wettbewerb entscheiden Prozessor A konkurriert nach der Arbitrierung (die Busarbitrierung stellt sicher, dass alle Prozessoren fairen Zugriff auf den Speicher haben). Zu diesem Zeitpunkt setzt Prozessor A seine Bustransaktion fort, während die anderen beiden Prozessoren auf den Abschluss der Bustransaktion von Prozessor A warten müssen, bevor sie erneut mit dem Speicherzugriff beginnen können. Nehmen Sie an, dass Prozessor A eine Bustransaktion ausführt (unabhängig davon, ob es sich bei der Bustransaktion um eine Lesetransaktion oder eine Schreibtransaktion handelt), Prozessor D eine Bustransaktion an den Bus initiiert. Zu diesem Zeitpunkt wird die Anforderung von Prozessor D vom Bus blockiert .
Diese Arbeitsmechanismen des Busses können den Zugriff aller Prozessoren auf den Speicher zu jedem Zeitpunkt auf serielle Weise durchführen, es kann jedoch höchstens ein Prozessor auf den Speicher zugreifen. Diese Funktion stellt sicher, dass Speicher-Lese-/Schreibvorgänge innerhalb einer einzelnen Bustransaktion atomar sind.
Wenn bei einigen 32-Bit-Prozessoren der Lese-/Schreibvorgang von 64-Bit-Daten atomar sein muss, entsteht ein relativ großer Overhead. Um sich um diese Art von Prozessor zu kümmern, fordert die Java-Sprachspezifikation die Atomizität der JVM beim Lesen/Schreiben von 64-Bit-langen Variablen und Double-Variablen, verlangt diese jedoch nicht. Wenn die JVM auf einem solchen Prozessor ausgeführt wird, teilt sie den Lese-/Schreibvorgang einer 64-Bit-Long/Double-Variable zur Ausführung in zwei 32-Bit-Lese-/Schreibvorgänge auf. Diese beiden 32-Bit-Lese-/Schreibvorgänge können zur Ausführung verschiedenen Bustransaktionen zugewiesen werden. Zu diesem Zeitpunkt ist das Lesen/Schreiben dieser 64-Bit-Variablen nicht atomar.
Wenn eine einzelne Speicheroperation nicht atomar ist, kann dies unerwartete Folgen haben. Schauen Sie sich bitte das Diagramm unten an:
Angenommen, wie in der Abbildung oben gezeigt, schreibt Prozessor A eine lange Variable und Prozessor B möchte diese lange Variable lesen. Der 64-Bit-Schreibvorgang in Prozessor A wird in zwei 32-Bit-Schreibvorgänge aufgeteilt, und die beiden 32-Bit-Schreibvorgänge werden zur Ausführung unterschiedlichen Schreibtransaktionen zugewiesen. Gleichzeitig wird der 64-Bit-Lesevorgang im Prozessor B in zwei 32-Bit-Lesevorgänge aufgeteilt und die beiden 32-Bit-Lesevorgänge werden zur Ausführung derselben Lesetransaktion zugewiesen. Wenn die Prozessoren A und B gemäß der Zeitsequenz in der Abbildung oben ausgeführt werden, sieht Prozessor B einen ungültigen Wert, der von Prozessor A nur „zur Hälfte geschrieben“ wurde.
Das Obige ist eine eingehende Analyse des Java-Speichermodells: Der Inhalt der sequentiellen Konsistenz. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (m.sbmmt.com)!