Heim  >  Artikel  >  Java  >  Detaillierte Einführung in das Java-Speichermodell

Detaillierte Einführung in das Java-Speichermodell

黄舟
黄舟Original
2017-02-28 10:41:421413Durchsuche

Dieses Java-Speichermodell gibt an, wie die Java Virtual Machine mit dem Computerspeicher (RAM) arbeitet. Bei dieser Java Virtual Machine handelt es sich um ein Modell des gesamten Computers, so dass dieses Modell natürlich auch ein Speichermodell – auch Java-Speichermodell genannt – beinhaltet.

Das Verständnis des Java-Speichermodells ist wichtig, wenn Sie gleichzeitige Programme korrekt entwerfen möchten. Dieses Java-Speichermodell bezieht sich darauf, wie und wann verschiedene Threads die Werte gemeinsam genutzter Variablen sehen können, die von anderen Threads geschrieben wurden, und wie synchron auf gemeinsam genutzte Variablen zugegriffen wird.

Das ursprüngliche Java-Speichermodell war so unzureichend, dass das Java-Speichermodell in Java 1.5 verbessert wurde. Diese Version des Java-Speichermodells wird weiterhin in Java 8 verwendet.

Internes Java-Speichermodell

Das Java-Speichermodell wird innerhalb der JVM verwendet, indem es in Thread-Stacks und Heaps unterteilt wird. Dieses Diagramm betrachtet das Speichermodell aus einer logischen Perspektive:


Jeder Thread, der in der Java Virtual Machine ausgeführt wird, verfügt über einen eigenen Thread-Stack. Der Thread-Stack enthält Informationen über die Methoden, die dieser Thread bis zum aktuellen Ausführungspunkt aufgerufen hat. Wir nennen dies auch „Call Stack“. Während der Thread seinen Code ausführt, ändert sich dieser Aufrufstapel.

Dieser Thread-Stapel enthält auch alle lokalen Variablen für jede ausgeführte Methode (alle Methoden auf dem Aufrufstapel). Ein Thread kann nur auf seinen eigenen Thread-Stack zugreifen. Von einem Thread erstellte lokale Variablen sind für alle anderen Threads nicht sichtbar. Selbst wenn zwei Threads genau denselben Code ausführen, erstellen beide Threads dennoch ihre eigenen lokalen Variablen. Daher verfügt jeder Thread über seine eigene Version lokaler Variablen.

Alle lokalen Variablen grundlegender Typen (boolean, byte, short, char, int, long, float, double) werden vollständig im Thread-Stack gespeichert und sind daher für andere Threads unsichtbar. Ein Thread kann eine Kopie einer Variablen eines primitiven Typs an einen anderen Thread übergeben, kann aber dennoch keine lokale Variable des primitiven Typs gemeinsam nutzen.

Dieser Heap enthält alle in Ihrer Anwendung erstellten Objekte, unabhängig davon, welcher Thread das Objekt erstellt hat. Dazu gehören Objektversionen von Basistypen (z. B. Byte, Integer, Long usw.). Unabhängig davon, ob ein Objekt erstellt und einer lokalen Variablen zugewiesen wird oder eine Mitgliedsvariable eines anderen Objekts erstellt wird, wird das Objekt weiterhin im Heap gespeichert.

Hier ist ein Diagramm, das diesen Aufrufstapel und die im Thread-Stack gespeicherten lokalen Variablen sowie die im Heap gespeicherten Objekte zeigt:


a Die Die lokale Variable kann ein primitiver Typ sein. In diesem Fall wird sie vollständig im Thread-Stack gespeichert.

Eine lokale Variable kann eine Objektreferenz sein. In diesem Szenario wird die Referenz (lokale Variable) im Thread-Stack gespeichert, das Objekt selbst jedoch im Heap.

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

Die Mitgliedsvariablen eines Objekts werden zusammen mit dem Objekt selbst im Heap gespeichert. Nicht nur, wenn diese Mitgliedsvariable vom Basistyp ist, sondern auch, wenn es sich um einen Verweis auf ein Objekt handelt.

Statische Klassenvariablen werden ebenfalls im Heap gespeichert.

Auf ein Objekt im Heap können alle Threads zugreifen, die eine Referenz auf dieses Objekt haben. Wenn ein Thread auf ein Objekt zugreift, kann er auch auf die Mitgliedsvariablen des Objekts zugreifen. Wenn zwei Threads gleichzeitig eine Methode für dasselbe Objekt aufrufen, greifen sie gleichzeitig auf die Mitgliedsvariablen des Objekts zu, aber jeder Thread verfügt über seine eigene Kopie der lokalen Variablen.

Hier ist eine Abbildung basierend auf der obigen Beschreibung:


Zwei Threads haben einen Satz lokaler Variablen. Eine der lokalen Variablen (Locale-Variable 2) zeigt auf ein gemeinsames Objekt im Heap (Objekt 3). Die beiden Threads haben jeweils einen unterschiedlichen Verweis auf dasselbe Objekt. Die lokalen Variablen, auf die sie verweisen, werden im Thread-Stack gespeichert, aber dasselbe Objekt, auf das diese beiden unterschiedlichen Referenzen verweisen, befindet sich im Heap.

Beachten Sie, wie dieses gemeinsame Objekt (Objekt 3) auf Objekt 2 und Objekt 4 als Mitgliedsvariablen verweist (in der Abbildung durch die Pfeile dargestellt). Durch die Referenzen dieser Variablen in Object3 können beide Threads auch auf Object2 und Object4 zugreifen.

Dieses Diagramm zeigt auch eine lokale Variable, die auf zwei verschiedene Objekte im Heap zeigt. In diesem Szenario verweist dieser Verweis auf zwei verschiedene Objekte (Objekt 1 und Objekt 5), nicht auf dasselbe Objekt. Theoretisch können zwei Objekte sowohl auf Objekt 1 als auch auf Objekt 5 zugreifen, wenn beide Threads Verweise auf diese beiden Objekte haben. Aber im Diagramm hat jeder Thread nur einen Verweis auf diese beiden Objekte.

Welche Art von Code wird also die Speicherstruktur im Bild oben haben? Nun, eine kurze Antwort 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 member1 = 67890;
}

Wenn zwei Threads diese Ausführungsmethode ausführen, zeigt dieses Symbol das Ergebnis früher an. Die run-Methode ruft die methodOne-Methode auf, und die methodOne-Methode ruft die methodTwo-Methode auf.

Die methodOne-Methode deklariert eine lokale Variable vom Basistyp (int-Typ) und eine lokale Variable der Objektreferenz.

Wenn jeder Thread die methodOne-Methode ausführt, erstellt er seine eigenen Kopien von localVariable1 und localVariable2 in ihren jeweiligen Thread-Stacks. Diese „localVariable1“ sind vollständig voneinander getrennt und bleiben nur in ihren jeweiligen Thread-Stacks bestehen. Ein Thread kann die von einem anderen Thread an „localVariable1“ vorgenommenen Änderungen nicht sehen.

Jeder Thread, der die methodOne-Methode ausführt, erstellt auch eine eigene Kopie von localVariable2. Diese beiden unterschiedlichen Kopien von localVariable2 verweisen jedoch auf dasselbe Objekt im Heap. Dieser Code legt localVariable2 so fest, dass es über eine statische Variable auf einen Verweis auf ein Objekt verweist. Es gibt nur eine Kopie der statischen Variablen, und diese Kopie befindet sich im Heap. Daher verweisen beide Kopien in localVariable2 letztendlich auf dieselbe Instanz. Dieses MySharedObject wird auch im Heap gespeichert. Es entspricht Objekt 3 im Bild oben.

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

Beachten Sie auch, wie die methodTwo-Methode eine lokale Variable von localVariable1 erstellt. Diese lokale Variable ist eine Referenz auf ein Integer-Objekt. Diese Methode legt den Verweis „localVariable1“ so fest, dass er auf eine neue Integer-Instanz verweist. Diese localVariable1-Referenz wird in einer Kopie jedes Threads in der ausführenden methodTwo-Methode gespeichert. Die beiden instanziierten Integer-Objekte werden im Heap gespeichert, aber jedes Mal, wenn diese Methode ausgeführt wird, wird ein neues Integer-Objekt erstellt, und die beiden Threads, die diese Methode ausführen, erstellen separate Integer-Instanzen. Die in der methodTwo-Methode erstellten Integer-Objekte entsprechen Objekt 1 und Objekt 5 in der obigen Abbildung.

Beachten Sie außerdem, dass die beiden Mitgliedsvariablen vom Typ long in der MySharedObject-Klasse Basistypen sind. Da es sich bei diesen Variablen um Mitgliedsvariablen handelt, werden sie weiterhin zusammen mit dem Objekt im Heap gespeichert. Im Thread-Stack werden nur lokale Variablen gespeichert.

Hardware-Speicherarchitektur

Die aktuelle Hardware-Speicherarchitektur unterscheidet sich geringfügig vom internen Java-Speichermodell. Es ist auch wichtig, die Hardware-Speicherarchitektur zu verstehen, und es ist hilfreich zu verstehen, wie das Java-Speichermodell funktioniert. In diesem Abschnitt wird das allgemeine Hardware-Speicher-Framework beschrieben, und in den folgenden Abschnitten wird beschrieben, wie das Java-Speichermodell damit funktioniert.

Hier ist ein vereinfachtes Diagramm der Hardwarestruktur eines modernen Computers:


Moderne Computer verfügen oft über zwei oder mehr CPUs. Einige dieser CPUs verfügen möglicherweise über mehrere Kerne. Der wichtige Punkt ist, dass auf Computern mit zwei oder mehr CPUs möglicherweise mehr als ein Thread gleichzeitig ausgeführt wird. Jede CPU kann zu jedem Zeitpunkt einen Thread ausführen. In Ihrer Java-Anwendung kann auf jeder CPU gleichzeitig ein Thread ausgeführt werden.

Jede CPU enthält eine Reihe von Registern, bei denen es sich im Wesentlichen um CPU-Speicher handelt. Diese CPU führt in Registern schneller aus als im Hauptspeicher. Das liegt daran, dass die CPU schneller auf Register zugreift als auf den Hauptspeicher.

Jede CPU kann auch über eine CPU-Cache-Speicherschicht verfügen. Tatsächlich verfügen die meisten modernen CPUs über eine Cache-Speicherschicht einigermaßen großer Größe. Diese CPU greift viel schneller auf die Cache-Speicherschicht zu als der Hauptspeicher, jedoch nicht so schnell wie auf interne Register. Dadurch liegt die Zugriffsgeschwindigkeit dieses CPU-Cache-Speichers zwischen internen Registern und dem Hauptspeicher. Einige CPUs verfügen möglicherweise über mehrere Cache-Ebenen (Ebene 1 und Ebene 2). Dies ist jedoch nicht wichtig, um die Interaktion des Java-Speichermodells mit dem Speicher zu verstehen. Es ist wichtig zu wissen, dass die CPU möglicherweise über eine Cache-Speicherschicht verfügt.

Ein Computer enthält auch einen Hauptspeicherbereich (RAM). Auf diesen Hauptspeicher haben alle CPUs Zugriff. Dieser Hauptspeicher ist typischerweise größer als der Cache-Speicher der CPU.

Wenn die CPU beispielsweise auf den Hauptspeicher zugreifen muss, liest sie den Hauptspeicherteil in den CPU-Cache. Möglicherweise liest es sogar Teile des Caches in Register ein und führt dort dann Operationen aus. Wenn die CPU das Ergebnis zurück in den Hauptspeicher schreiben muss, löscht sie den Wert aus dem internen Register in den Cache-Speicher und irgendwann auch in den Hauptspeicher.

Diese im Cache-Speicher gespeicherten Werte werden in den Hauptspeicher übertragen, wenn die CPU dort etwas anderes speichern muss. Dieser CPU-Cache kann manchmal in einen Teil seines Speichers geschrieben werden, und manchmal kann ein Teil seines Speichers geleert werden. Sie muss nicht jedes Mal den gesamten Cache lesen und schreiben. Normalerweise wird dieser Cache in kleineren Speicherblöcken, sogenannten „Cache-Zeilen“, aktualisiert. Eine oder mehrere Cache-Zeilen können in den Cache-Speicher eingelesen werden und eine oder mehrere Cache-Zeilen können erneut in den Hauptspeicher geleert werden.

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

Wie bereits erwähnt, sind Java-Speichermodell und Hardware-Speicherstruktur unterschiedlich. Diese Hardware-Speicherstruktur unterscheidet nicht zwischen Thread-Stacks und Heaps. Bei der Hardware befinden sich sowohl der Thread-Stack als auch der Heap im Hauptspeicher. Teile des Thread-Stacks und Heaps können manchmal im CPU-Cache und in den internen CPU-Registern angezeigt werden, wie in der folgenden Abbildung dargestellt:


Wenn Objekte und Variablen bestimmte Probleme haben kann auftreten, wenn Daten in verschiedenen Speicherbereichen Ihres Computers gespeichert werden können. Die beiden Hauptprobleme sind:


  • Thread-Sichtbarkeit für Aktualisierungen gemeinsamer Variablen

  • Beim Lesen der Rennbedingungen für Abrufen, Überprüfen und Schreiben von gemeinsam genutzten Variablen

Diese Probleme werden in den folgenden Abschnitten erläutert.


Sichtbarkeit gemeinsamer Objekte


Wenn zwei Oder wenn Mehrere Threads teilen sich ein Objekt. Ohne ordnungsgemäße Verwendung flüchtiger Deklarationen oder Synchronisierung sind von einem Thread aktualisierte gemeinsam genutzte Variablen möglicherweise für andere Threads nicht sichtbar.

Stellen Sie sich vor, dass das gemeinsame Objekt zunächst im Hauptspeicher gespeichert ist. Ein auf der CPU ausgeführter Thread liest das gemeinsam genutzte Objekt in seinen CPU-Cache. Hier wird eine Änderung an einem gemeinsam genutzten Objekt vorgenommen. Solange der CPU-Cache nicht in den Hauptspeicher geleert wird, ist die geänderte Version dieses gemeinsam genutzten Objekts für Threads, die auf anderen CPUs laufen, nicht sichtbar. Auf diese Weise erhält möglicherweise jeder Thread eine eigene Kopie des gemeinsam genutzten Objekts, wobei sich jede Kopie in einem anderen CPU-Cache befindet.

Das folgende Diagramm veranschaulicht die schematische Situation. Ein Thread, der auf der linken CPU läuft, kopiert die gemeinsam genutzte Variable in den CPU-Cache und ändert ihren Wert auf 2. Diese Änderung ist für andere Threads, die auf der rechten CPU ausgeführt werden, nicht sichtbar, da die Aktualisierung der Zählung noch nicht in den Hauptspeicher zurückgespült wurde.

Um dieses Problem zu lösen, können Sie das Schlüsselwort volatile von Java verwenden. Dieses Schlüsselwort stellt sicher, dass eine bestimmte Variable bei Aktualisierung direkt aus dem Hauptspeicher gelesen und direkt in den Hauptspeicher geschrieben wird.

Race-Bedingung

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

Stellen Sie sich vor, Thread A liest die Zählvariable eines gemeinsam genutzten Objekts in seinen CPU-Cache. In der Zwischenzeit macht Thread B dasselbe, geht aber in einen anderen CPU-Cache. Jetzt werden Thread-Inkremente um eins gezählt, und Thread B macht dasselbe. Jetzt wird die Variable zweimal erhöht.

Wenn diese Inkremente nacheinander durchgeführt werden, wird die Zählvariable zweimal erhöht und plus 2 wird basierend auf dem ursprünglichen Wert in den Hauptspeicher geschrieben.

Dann sind die beiden Inkremente nicht richtig synchronisiert, was zu einer gleichzeitigen Ausführung führt. Unabhängig davon, ob Thread A oder Thread B ihr Update in den Hauptspeicher schreibt, wird der Wert dieses Updates nur um 1 und nicht um 2 erhöht.

Dieses Diagramm zeigt das oben beschriebene Problem mit der Race-Bedingung:

Um dieses Problem zu lösen, können Sie Java-Synchronisationssperren verwenden. Durch eine Synchronisationssperre kann sichergestellt werden, dass jeweils nur ein Thread den kritischen Bereich des Codes betreten kann. Die Synchronisationssperre stellt außerdem sicher, dass alle Variablenzugriffe aus dem Hauptspeicher gelesen werden. Wenn der Thread den synchronisierten Codeblock verlässt, werden alle aktualisierten Variablen wieder in den Hauptspeicher zurückgespült, unabhängig davon, ob die Variable als flüchtig deklariert ist.

Das Obige ist die detaillierte Einführung des Java-Speichermodells. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn