Multithreading. Java-Speichermodell (Teil 2)

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des zweiten Teils des Artikels "Java Memory Model" von Jakob Jenkov. Der erste Teil ist hier .



Speicherhardwarearchitektur



Die moderne Speicherhardwarearchitektur unterscheidet sich etwas vom internen Java-Speichermodell. Es ist wichtig, die Hardwarearchitektur zu verstehen, um zu verstehen, wie das Java-Modell damit funktioniert. In diesem Abschnitt wird die allgemeine Speicherhardwarearchitektur beschrieben, und im nächsten Abschnitt wird beschrieben, wie Java damit arbeitet.



Hier ist ein vereinfachtes Diagramm der Hardwarearchitektur eines modernen Computers:



Ein moderner Computer verfügt häufig über zwei oder mehr Prozessoren. Einige dieser Prozessoren können auch mehrere Kerne haben. Auf solchen Computern können mehrere Threads gleichzeitig ausgeführt werden. Jeder Prozessor (Anmerkung des Übersetzers - im Folgenden meint der Autor wahrscheinlich einen Prozessorkern oder einen Einkernprozessor durch einen Prozessor)kann jeweils einen Thread ausführen. Dies bedeutet, dass in Ihrem Programm ein Thread pro Prozessor gleichzeitig ausgeführt werden kann, wenn Ihre Java-Anwendung Multithreading ist.



Jeder Prozessor enthält einen Satz von Registern, die sich im Wesentlichen in seinem Speicher befinden. Es kann Operationen an Daten in Registern viel schneller ausführen als an Daten, die sich im Hauptspeicher (RAM) des Computers befinden. Dies liegt daran, dass der Prozessor viel schneller auf diese Register zugreifen kann.



Jede CPU kann auch eine Cache-Schicht haben. In der Tat haben die meisten modernen Prozessoren es. Ein Prozessor kann viel schneller als der Hauptspeicher auf seinen Cache-Speicher zugreifen, jedoch im Allgemeinen nicht so schnell wie seine internen Register. Somit liegt die Geschwindigkeit des Zugriffs auf den Cache-Speicher irgendwo zwischen der Geschwindigkeit des Zugriffs auf die internen Register und auf den Hauptspeicher. Einige Prozessoren verfügen möglicherweise über abgestufte Caches. Dies ist jedoch nicht wichtig, um zu verstehen, wie das Java-Speichermodell mit dem Hardwarespeicher interagiert. Es ist wichtig zu wissen, dass Prozessoren über einen gewissen Cache-Speicher verfügen können.



Der Computer enthält auch einen Bereich des Hauptspeichers (RAM). Alle Prozessoren können auf den Hauptspeicher zugreifen. Der Hauptspeicherbereich ist normalerweise viel größer als der Cache des Prozessors.



Wenn ein Prozessor Zugriff auf den Hauptspeicher benötigt, liest er normalerweise einen Teil davon in seinen Cache-Speicher. Es kann auch einige Daten aus dem Cache in seine internen Register einlesen und dann Operationen an ihnen ausführen. Wenn die CPU ein Ergebnis in den Hauptspeicher zurückschreiben muss, werden Daten aus ihrem internen Register in den Cache-Speicher und irgendwann in den Hauptspeicher gelöscht.



Im Cache gespeicherte Daten werden normalerweise in den Hauptspeicher zurückgespült, wenn der Prozessor etwas anderes im Cache speichern muss. Der Cache kann seinen Speicher löschen und gleichzeitig neue Daten in ihn schreiben. Der Prozessor muss nicht bei jeder Aktualisierung den gesamten Cache lesen / schreiben. Normalerweise wird der Cache in kleinen Speicherblöcken aktualisiert, die als "Cache-Zeilen" bezeichnet werden. Eine oder mehrere Cache-Zeilen können in den Cache-Speicher eingelesen werden, und eine oder mehrere Cache-Zeilen können in den Hauptspeicher zurückgespült werden.



Kombination von Java-Speichermodell und Hardware-Speicherarchitektur



Wie bereits erwähnt, unterscheiden sich das Java-Speichermodell und die Speicherhardwarearchitektur. Die Hardwarearchitektur unterscheidet nicht zwischen Thread-Stack und Heap. Auf der Hardware befinden sich der Thread-Stapel und der Heap im Hauptspeicher. Teile von Stapeln und Thread-Heaps können manchmal in Caches und internen Registern der CPU vorhanden sein. Dies ist im Diagramm dargestellt:



Wenn Objekte und Variablen in verschiedenen Bereichen des Computerspeichers gespeichert werden können, können bestimmte Probleme auftreten. Es gibt zwei Hauptpunkte:

• Sichtbarkeit der Änderungen, die der Thread an gemeinsam genutzten Variablen vorgenommen hat.

• Rennbedingungen beim Lesen, Überprüfen und Schreiben gemeinsamer Variablen.

Diese beiden Probleme werden in den folgenden Abschnitten erläutert.



Sichtbarkeit von gemeinsam genutzten Objekten



Wenn zwei oder mehr Threads ein Objekt ohne ordnungsgemäße flüchtige Deklaration oder Synchronisierung miteinander teilen, sind Änderungen an dem gemeinsam genutzten Objekt, die von einem Thread vorgenommen wurden, für andere Threads möglicherweise nicht sichtbar.



Stellen Sie sich vor, ein gemeinsam genutztes Objekt wird zunächst im Hauptspeicher gespeichert. Ein Thread, der auf einer CPU ausgeführt wird, liest ein freigegebenes Objekt in den Cache derselben CPU. Dort nimmt er Änderungen am Objekt vor. Bis der CPU-Cache in den Hauptspeicher geleert wurde, ist die geänderte Version des gemeinsam genutzten Objekts für Threads, die auf anderen CPUs ausgeführt werden, nicht sichtbar. Auf diese Weise kann jeder Thread eine eigene Kopie des gemeinsam genutzten Objekts erhalten. Jede Kopie befindet sich in einem separaten CPU-Cache.



Das folgende Diagramm zeigt eine Skizze dieser Situation. Ein Thread, der auf der linken CPU ausgeführt wird, kopiert das freigegebene Objekt in seinen Cache und ändert den Wert der VariablencountDiese Änderung ist für andere Threads, die auf der richtigen CPU ausgeführt werden, countnicht sichtbar, da das Update für noch nicht in den Hauptspeicher zurückgespült wurde.



Um dieses Problem zu lösen, können Sie volatilebeim Deklarieren einer Variablen verwenden. Es kann sicherstellen, dass eine bestimmte Variable direkt aus dem Hauptspeicher gelesen und bei der Aktualisierung immer in den Hauptspeicher zurückgeschrieben wird.



Rennbedingung



Wenn zwei oder mehr Threads dasselbe Objekt und mehr als eine Thread-Aktualisierungsvariable in diesem gemeinsam genutzten Objekt verwenden, kann eine Race-Bedingung auftreten .



Stellen Sie sich vor, Thread A liest eine countgemeinsam genutzte Objektvariable in den Cache seines Prozessors. Stellen Sie sich auch vor, dass Thread B dasselbe tut, jedoch den Cache eines anderen Prozessors. Jetzt addiert Thread A 1 zum Wert der Variablen count, und Thread B macht dasselbe. Jetzt wurde es var1zweimal erhöht - separat, +1 im Cache jedes Prozessors.



Wenn diese Inkremente nacheinander ausgeführt würden, würde die Variable countverdoppelt und in den Hauptspeicher zurückgeschrieben + 2.

Die beiden Inkremente wurden jedoch gleichzeitig ohne ordnungsgemäße Synchronisation durchgeführt. Unabhängig davon, welcher Thread (A oder B) seine aktualisierte Version countin den Hauptspeicher schreibt , ist der neue Wert trotz zweier Inkremente nur 1 mehr als der ursprüngliche Wert.



Dieses Diagramm zeigt das Auftreten des oben beschriebenen Race Condition-Problems:



Um dieses Problem zu lösen, können Sie einen synchronisierten Java-Block verwenden... Ein synchronisierter Block stellt sicher, dass immer nur ein Thread einen bestimmten kritischen Codeabschnitt eingeben kann. Synchronisierte Blöcke stellen außerdem sicher, dass alle Variablen, auf die innerhalb eines synchronisierten Blocks zugegriffen wird, aus dem Hauptspeicher gelesen werden. Wenn ein Thread einen synchronisierten Block verlässt, werden alle aktualisierten Variablen in den Hauptspeicher zurückgespült, unabhängig davon, ob die Variable als deklariert ist volatileoder nicht. ...



All Articles