Übersicht über das Java Memory Model (JMM)

Java-Speichermodell (Java-Speichermodell)

Transfer von Jenkins

Das Java-Speichermodell gibt an, wie die Java Virtual Machine mit dem Arbeitsspeicher (RAM) des Computers verwendet wird. Die Java Virtual Machine ist ein Modell des gesamten Computers, daher enthält das Modell natürlich ein Speichermodell - AKA Java-Speichermodell.

Wenn Sie ein Programm mit gleichzeitigem Verhalten ordnungsgemäß entwerfen möchten, ist es sehr wichtig, das Java-Speichermodell zu verstehen. Das Java-Speichermodell gibt an, wie und wann verschiedene Threads die Werte sehen, die von anderen Threads in gemeinsam genutzte Variablen geschrieben wurden, und wie der Zugriff auf gemeinsam genutzte Variablen bei Bedarf synchronisiert wird.

Das ursprüngliche Java-Speichermodell war unzureichend, daher wurde das Java-Speichermodell in Java 1.5 überarbeitet. Diese Version des Java-Speichermodells wird heute noch in Java (Java 14+) verwendet.

Internes Java-Speichermodell

Das von der JVM intern verwendete Java-Speichermodell weist Speicher zwischen dem Thread-Stapel und dem Heap zu. Diese Abbildung zeigt das Java-Speichermodell aus einer logischen Perspektive:

Fügen Sie hier eine Bildbeschreibung ein
Jeder Thread, der in der Java Virtual Machine ausgeführt wird, verfügt über einen eigenen Thread-Stack. Der Thread-Stapel enthält Informationen darüber, welche Methoden der Thread aufgerufen hat, um den aktuellen Ausführungspunkt zu erreichen. Ich nenne es den "Call Stack". Wenn ein Thread seinen Code ausführt, ändert sich der Aufrufstapel.

Der Thread-Stapel enthält auch alle lokalen Variablen jeder ausgeführten Methode (alle Methoden im Aufrufstapel). Ein Thread kann nur auf seinen eigenen Thread-Stapel zugreifen. Von einem Thread erstellte lokale Variablen sind für alle anderen Threads außer dem Erstellungs-Thread unsichtbar. Selbst wenn der von den beiden Threads ausgeführte Code genau der gleiche ist, erstellen die beiden Threads dennoch lokale Variablen des Codes in ihren jeweiligen Thread-Stapeln. Daher hat jeder Thread eine eigene Version jeder lokalen Variablen.

Alle lokalen Variablen primitiver Typen (Boolescher Wert, Byte, Kurzwert, Zeichen, Int, Long, Float, Double) werden vollständig im Thread-Stapel gespeichert, sodass sie für andere Threads nicht sichtbar sind. Ein Thread kann eine Kopie einer Hauptvariablen an einen anderen Thread übergeben, jedoch nicht die ursprüngliche lokale Variable selbst freigeben.

Der Heap enthält alle in einer Java-Anwendung erstellten Objekte, unabhängig vom Thread, der das Objekt erstellt hat. Dies umfasst primitive Typen (z. B. Objektversionen Byte, Integer, Long usw.). Es spielt keine Rolle, ob Sie ein Objekt erstellen und einer lokalen Variablen zuweisen oder es als Mitgliedsvariable eines anderen Objekts erstellen. Das Objekt wird weiterhin im Heap gespeichert.

Dies ist ein Diagramm, das den Aufrufstapel, die auf dem Thread-Stapel gespeicherten
Fügen Sie hier eine Bildbeschreibung ein
lokalen Variablen und die auf dem Heap gespeicherten Objekte darstellt: Lokale Variablen können primitive Typen sein. In diesem Fall verbleibt sie vollständig im Thread-Stapel.

Lokale Variablen können auch Verweise auf Objekte sein. In diesem Fall wird die Referenz (lokale Variable) im Thread-Stapel gespeichert, aber das Objekt selbst (falls im Heap gespeichert).

Ein Objekt kann Methoden enthalten, und diese Methoden können lokale Variablen enthalten. Diese lokalen Variablen werden auch im Thread-Stapel gespeichert, selbst wenn das Objekt, zu dem die Methode gehört, im Heap gespeichert ist.

Die Mitgliedsvariablen des Objekts werden zusammen mit dem Objekt selbst im Heap gespeichert. Dies gilt, wenn die Elementvariable ein primitiver Typ ist und wenn sie auf ein Objekt verweist.

Statische Klassenvariablen werden zusammen mit der Klassendefinition auch im Heap gespeichert.

Alle Threads, die auf das Objekt verweisen, können auf das Objekt auf dem Heap zugreifen. Wenn ein Thread auf ein Objekt zugreifen kann, kann er auch auf die Mitgliedsvariablen des Objekts zugreifen. Wenn zwei Threads gleichzeitig eine Methode für dasselbe Objekt aufrufen, haben beide Zugriff auf die Elementvariablen des Objekts, aber jeder Thread verfügt über eine eigene Kopie der lokalen Variablen.

Dies ist ein Diagramm, das die obigen Punkte veranschaulicht:
Fügen Sie hier eine Bildbeschreibung ein
Zwei Threads haben eine Reihe lokaler Variablen. Eine der lokalen Variablen (Local Variable 2) zeigt auf ein gemeinsam genutztes Objekt (Object 3) auf dem Heap. Diese beiden Threads haben jeweils unterschiedliche Verweise auf dasselbe Objekt. Ihre Referenzen sind lokale Variablen, daher werden sie im Thread-Stapel jedes Threads (auf jedem Thread) gespeichert. Zwei verschiedene Verweise verweisen jedoch auf dasselbe Objekt auf dem Heap.

Beachten Sie, wie das gemeinsam genutzte Objekt (Objekt 3) auf Objekt 2 und Objekt 4 als Elementvariablen verweist (wie durch die Pfeile von Objekt 3 zu Objekt 2 und Objekt 4 dargestellt). Über diese Mitgliedsvariablenreferenzen in Objekt 3 können zwei Threads auf Objekt 2 und Objekt 4 zugreifen.

Die Abbildung zeigt auch eine lokale Variable, die auf zwei verschiedene Objekte auf dem Heap verweist. In diesem Fall verweist die Referenz auf zwei verschiedene Objekte (Objekt 1 und Objekt 5), nicht auf dasselbe Objekt. Wenn zwei Threads beide auf zwei Objekte verweisen, können beide Threads theoretisch auf Objekt 1 und Objekt 5 zugreifen. In der obigen Abbildung verweist jeder Thread jedoch nur auf eines der beiden Objekte.

Welche Art von Java-Code kann das obige Speicherdiagramm verursachen? Nun, der Code ist so einfach wie der folgende Code:

public class MyRunnable implements Runnable() {
    
    

    public void run() {
    
    
        methodOne();
    }

    public void methodOne() {
    
    
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
    
    
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}

public class MySharedObject {
    
    

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member2 = 67890;
}

Wenn zwei Threads die run () -Methode ausführen, ist das Ergebnis wie zuvor. Die Methode run () ruft methodOne () auf und methodOne () ruft methodTwo () auf.

methodOne () deklariert eine lokale Basisvariable (localVariable1 Typ int) und eine lokale Variable, die eine Objektreferenz (localVariable2) ist.

Jeder Thread, der methodOne () ausführt, erstellt eine eigene Kopie, localVariable1 und localVariable2, in ihren jeweiligen Thread-Stapeln. Diese localVariable1-Variablen werden vollständig voneinander getrennt und sind nur im Thread-Stapel jedes Threads vorhanden. Ein Thread kann die Änderungen, die ein anderer Thread an seiner Kopie localVariable1 vorgenommen hat, nicht sehen.

Jeder Thread der AusführungsmethodeOne () erstellt auch eine eigene Kopie von localVariable2. Zwei unterschiedliche Kopien der beiden localVariable2 verweisen jedoch auf dasselbe Objekt auf dem Heap. Dieser Code setzt localVariable2 so, dass es auf das Objekt verweist, auf das die statische Variable verweist. Es gibt nur eine Kopie einer statischen Variablen, und diese Kopie wird im Heap gespeichert. Daher verweisen beide localVariable2-Endkopien auf dieselbe Instanz, auf die die statische Variable MySharedObject verweist. Die MySharedObject-Instanz wird ebenfalls im Heap gespeichert. Es entspricht Objekt 3 in der obigen Abbildung.

Beachten Sie, dass die MySharedObject-Klasse auch zwei Elementvariablen enthält. Die Mitgliedsvariable selbst wird zusammen mit dem Objekt im Heap gespeichert. Diese beiden Elementvariablen zeigen auf zwei andere Integer-Objekte. Diese Integer-Objekte entsprechen Objekt 2 und Objekt 4 in der obigen Abbildung.

Beachten Sie auch, wie methodTwo () eine lokale Variable mit dem Namen localVariable1 erstellt. Diese lokale Variable ist eine Objektreferenz Integer zum Objekt. Diese Methode setzt die localVariable1-Referenz so, dass sie auf die neue Integer-Instanz verweist. Die localVariable1-Referenz wird in einer Kopie von methodTwo () in jedem Ausführungsthread gespeichert. Die beiden durch Integer instanziierten Objekte werden auf dem Heap gespeichert. Da die Methode Integer jedoch bei jeder Ausführung ein neues Objekt erstellt, erstellen die beiden Threads, die diese Methode ausführen, separate Integer-Instanzen. Das in Integer erstellte Objekt methodTwo () entspricht Objekt 1 und Objekt 5 in der obigen Abbildung.

Beachten Sie auch, dass die beiden Elementvariablen in der MySharedObject-Typklasse, solange sie primitive Typen sind. Da diese Variablen Mitgliedsvariablen sind, werden sie zusammen mit dem Objekt im Heap gespeichert. Auf dem Thread-Stapel werden nur lokale Variablen gespeichert.

Hardware-Speicherarchitektur

Die moderne Hardware-Speicherarchitektur unterscheidet sich vom internen Java-Speichermodell. Es ist auch wichtig, die Hardware-Speicherarchitektur zu verstehen und zu verstehen, wie das Java-Speichermodell damit funktioniert. In diesem Abschnitt wird die allgemeine Hardware-Speicherarchitektur beschrieben, und im nächsten Abschnitt wird beschrieben, wie das Java-Speichermodell damit funktioniert.

Dies ist ein vereinfachtes Diagramm der modernen Computerhardwarearchitektur:
Fügen Sie hier eine Bildbeschreibung ein

Moderne Computer enthalten normalerweise 2 oder mehr CPUs. Einige dieser CPUs können auch mehrere Kerne haben. Der Punkt ist, dass auf modernen Computern mit 2 oder mehr CPUs mehrere Threads gleichzeitig ausgeführt werden können. Jede CPU kann jederzeit einen Thread ausführen. Dies bedeutet, dass, wenn die Java-Anwendung über mehrere Threads verfügt, jede CPU gleichzeitig (gleichzeitig) einen Thread in der Java-Anwendung ausführen kann.

Jede CPU enthält einen Satz von Registern, die im Wesentlichen CPU-Speicher sind. Die CPU führt Operationen an diesen Registern viel schneller aus als die Variablen im Hauptspeicher. Dies liegt daran, dass die CPU schneller als der Hauptspeicher auf diese Register zugreifen kann.

Jede CPU kann auch eine CPU-Cache-Speicherschicht haben. Tatsächlich haben die meisten modernen CPUs eine bestimmte Größe der Cache-Schicht. Die CPU kann schneller als der Hauptspeicher auf ihren Cache zugreifen, ist jedoch im Allgemeinen nicht so schnell wie der Zugriff auf interne Register. Daher befindet sich der CPU-Cache-Speicher zwischen den internen Registern und der Geschwindigkeit des Hauptspeichers. Einige CPUs verfügen möglicherweise über mehrere Cache-Ebenen (Ebene 1 und Ebene 2). Es ist jedoch nicht wichtig zu verstehen, wie das Java-Speichermodell mit dem Speicher interagiert. Es ist wichtig zu wissen, dass die CPU eine Art Cache-Schicht haben kann.

Der Computer enthält auch einen Hauptspeicherbereich (RAM). Alle CPUs können auf den Hauptspeicher zugreifen. Der Hauptspeicherbereich ist normalerweise viel größer als der CPU-Cache.

Wenn die CPU auf den Hauptspeicher zugreifen muss, liest sie normalerweise einen Teil des Hauptspeichers in ihren CPU-Cache. Es kann sogar einen Teil des Caches in seine internen Register einlesen und dann Operationen daran ausführen. Wenn die CPU das Ergebnis in den Hauptspeicher zurückschreiben muss, wird der Wert aus ihrem internen Register in den Cache und dann irgendwann in den Hauptspeicher zurückgespült.

Wenn die CPU anderen Inhalt im Cache speichern muss, wird der im Cache gespeicherte Wert normalerweise in den Hauptspeicher zurückgespült. Der CPU-Cache kann Daten gleichzeitig in einen Teil seines Speichers schreiben und jeweils einen Teil seines Speichers aktualisieren. Es muss nicht bei jeder Aktualisierung den gesamten Cache lesen / schreiben. Im Allgemeinen wird der Cache in kleineren 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 wieder in den Hauptspeicher zurückgespült werden.

Überbrückung der Lücke zwischen Java-Speichermodell und Hardware-Speicherarchitektur

Wie bereits erwähnt, unterscheiden sich das Java-Speichermodell und die Hardware-Speicherarchitektur. Die Hardware-Speicherarchitektur unterscheidet nicht zwischen Thread-Stack und Heap. Auf der Hardware befinden sich der Thread-Stapel und der Heap im Hauptspeicher. Ein Teil des Thread-Stapels und des Heaps kann manchmal im CPU-Cache und in den internen CPU-Registern erscheinen. Die folgende Abbildung veranschaulicht dies:
Fügen Sie hier eine Bildbeschreibung ein
Wenn Objekte und Variablen in verschiedenen Speicherbereichen des Computers gespeichert werden können, können bestimmte Probleme auftreten. Die zwei Hauptprobleme sind:

  • Thread-Updates (Schreibvorgänge) zur Sichtbarkeit gemeinsam genutzter Variablen.
  • 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 gemeinsam nutzen, ohne die flüchtige Deklaration oder Synchronisation korrekt zu verwenden, sind die von einem Thread am gemeinsam genutzten Objekt vorgenommenen Aktualisierungen für andere Threads möglicherweise nicht sichtbar.

Stellen Sie sich vor, das gemeinsam genutzte Objekt wurde ursprünglich im Hauptspeicher gespeichert. Dann liest der auf der CPU laufende Thread das gemeinsam genutzte Objekt in seinen CPU-Cache. Dort wurde die gemeinsam genutzte Bibliothek geändert. Solange der CPU-Cache nicht in den Hauptspeicher zurückgespült wird, können Threads, die auf anderen CPUs ausgeführt werden, die geänderte Version des gemeinsam genutzten Objekts nicht sehen. Auf diese Weise kann jeder Thread möglicherweise eine eigene Kopie der gemeinsam genutzten Bibliothek haben, wobei sich jede Kopie in einem anderen CPU-Cache befindet.

Die folgende Abbildung zeigt diese Situation. Ein Thread, der auf der linken CPU ausgeführt wird, kopiert die gemeinsam genutzte Bibliothek in den CPU-Cache und ändert die Anzahl der Variablen in 2. Die anderen Threads, die auf der rechten CPU ausgeführt werden, können diese Änderung nicht sehen, da count das Update nicht in den Hauptspeicher zurückgespült hat.
Fügen Sie hier eine Bildbeschreibung ein

Um dieses Problem zu lösen, können Sie das flüchtige Schlüsselwort von Java verwenden . Das flüchtige Schlüsselwort kann sicherstellen, dass eine bestimmte Variable immer in den Hauptspeicher zurückgeschrieben wird, wenn sie direkt aus dem Hauptspeicher gelesen und aktualisiert wird.

Wettbewerbsbedingungen

Wenn zwei oder mehr Threads ein Objekt gemeinsam nutzen und mehr als ein Thread Variablen im gemeinsam genutzten Objekt aktualisiert, kann eine Race-Bedingung auftreten .

Stellen Sie sich vor, Thread A liest die Variable der gemeinsam genutzten Zählbibliothek in ihren CPU-Cache. Stellen Sie sich auch vor, dass Thread B dieselbe Funktion hat, sich jedoch in einem anderen CPU-Cache befindet. Jetzt fügt Thread A eine Zählung hinzu, und Thread B führt dieselbe Operation aus. Jetzt wurde var1 zweimal erhöht, einmal pro CPU-Cache.

Wenn diese Inkremente nacheinander ausgeführt werden, wird die Anzahl der Variablen zweimal erhöht und der ursprüngliche Wert + 2 in den Hauptspeicher zurückgeschrieben.

Diese beiden Inkremente werden jedoch gleichzeitig ohne ordnungsgemäße Synchronisation ausgeführt. Unabhängig davon, welcher Thread A oder B seine aktualisierte Version in den Hauptspeicher zurückschreibt, ist der aktualisierte Wert nur 1 höher als der ursprüngliche Wert, obwohl es zwei Inkremente gibt.

Diese Abbildung zeigt das Auftreten des oben beschriebenen Race Condition-Problems:
Fügen Sie hier eine Bildbeschreibung ein
Um dieses Problem zu lösen, können Sie den Java-Synchronisationsblock verwenden . Der Synchronisationsblock garantiert, dass immer nur ein Thread einen bestimmten kritischen Teil des Codes eingeben kann. Der Synchronisationsblock garantiert auch, dass alle Variablen, auf die im Synchronisationsblock zugegriffen wird, aus dem Hauptspeicher gelesen werden. Wenn der Thread den Synchronisationsblock verlässt, werden alle aktualisierten Variablen wieder in den Hauptspeicher zurückgespült, unabhängig davon, ob die Variable als flüchtig deklariert ist.

Ich denke du magst

Origin blog.csdn.net/e891377/article/details/108686228
Empfohlen
Rangfolge