Das Java-Speichermodell, das als JMM bezeichnet wird, ist eine einheitliche Garantie für eine Reihe von Java Virtual Machine-Plattformen für die nicht verwandte Plattform für die Sichtbarkeit von Speicher und ob es in einer von Entwicklern bereitgestellten Umgebung mit mehreren Threaden neu angeordnet werden kann. (Es kann mehrdeutig in Bezug auf den Begriff und die Speicherverteilung der Java -Laufzeit sein, die sich auf Speicherbereiche wie Heap, Methodenbereich, Thread -Stapel usw. bezieht).
Es gibt viele Arten der gleichzeitigen Programmierung. Zusätzlich zu CSP (Kommunikationssequentialprozess), Akteur und anderen Modellen sollte das bekannteste Modell auf der Grundlage von Threads und Schlössern das gemeinsame Speichermodell sein. Bei der Programmierung mit mehreren Threads müssen drei Arten von Parallelitätsproblemen beachtet werden:
・ Atomizität ・ Sichtbarkeit ・ Neuordnung
Atomizität umfasst, ob andere Threads den Zwischenzustand sehen oder sich stören können, wenn ein Faden einen zusammengesetzten Betrieb ausführt. Normalerweise ist es das Problem von i ++. Zwei Threads führen gleichzeitig ++ Operationen auf dem gemeinsam genutzten Heap -Speicher aus. Die Implementierung von ++ Operationen in JVM, Laufzeit und CPU kann ein zusammengesetzter Betrieb sein. Aus der Perspektive der JVM -Anweisungen soll beispielsweise der Wert von I aus dem Heap -Speicher zum Operand -Stapel gelesen, einen hinzufügen und zum Heap -Speicher zurückschreiben. Während dieser Operationen können andere Threads sie gleichzeitig ausführen, wenn es keine korrekte Synchronisation gibt, was zu Datenverlust und anderen Problemen führen kann. Häufige Atomizitätsprobleme, auch als Wettbewerbszustand bezeichnet, werden anhand eines möglichen Versagensergebnisses wie Read-Modify-Write beurteilt. Sichtbarkeits- und Nachbestellprobleme sind beide von der Systemoptimierung.
Da die Ausführungsgeschwindigkeit der CPU und die Zugriffsgeschwindigkeit des Speichers ernsthaft nicht übereinstimmend sind, um die Leistung zu optimieren, basiert sie auf Lokalisierungsprinzipien wie Zeitlokalität und räumliche Lokalität, die CPU hat einen mehrstöckigen Cache zwischen dem Speicher hinzugefügt. Wenn es erforderlich ist, Daten abzurufen, geht die CPU zuerst in den Cache, um herauszufinden, ob der entsprechende Cache vorhanden ist. Wenn es existiert, wird es direkt zurückgegeben. Wenn es nicht existiert, wird es in den Speicher eingereicht und im Cache gespeichert. Jetzt sind die Multi-Kern-Prozessoren zu Standard, jeder Prozessor hat seinen eigenen Cache, der das Problem der Cache-Konsistenz beinhaltet. CPUs haben Konsistenzmodelle unterschiedlicher Stärken und Schwächen. Die stärkste Konsistenz ist die höchste Sicherheit und entspricht auch unserem sequentiellen Denkmodus. In Bezug auf die Leistung wird es jedoch viel Overhead geben, da eine koordinierte Kommunikation zwischen verschiedenen CPUs erforderlich ist.
Ein typisches CPU -Cache -Strukturdiagramm ist wie folgt
Der Anweisungszyklus der CPU ist in der Regel das Abrufen von Anweisungen, Anweisungen zum Lesen von Daten, das Ausführen von Anweisungen und das Schreiben von Daten in Register oder Speicher. Bei der Ausführung von Anweisungen in seriell werden die Lese- und gespeicherten Daten lange Zeit angelegt, sodass die CPU im Allgemeinen die Anweisungspipeline verwendet, um mehrere Anweisungen gleichzeitig auszuführen, um den Gesamtdurchsatz genau wie eine Fabrikpipeline zu verbessern.
Die Geschwindigkeit des Lesens von Daten und zum Zurückschreiben von Daten in den Speicher liegt nicht in der gleichen Größenordnung wie die Ausführung von Anweisungen, sodass die CPU Register und Caches als Caches und Puffer verwendet. Beim Lesen von Daten aus dem Speicher wird eine Cache -Zeile gelesen (ähnlich wie bei der Festplatte und einem Block). Das Modul, das Daten zurückschreibt, wird die Speicheranforderung in einen Speicherpuffer einfügen, wenn sich die alten Daten nicht im Cache befinden, und führt weiterhin die nächste Stufe des Anweisungszyklus aus. Wenn es im Cache vorhanden ist, wird der Cache aktualisiert und die Daten im Cache werden gemäß einer bestimmten Richtlinie zum Speicher speichern.
public Class memorModel {private int count; privater boolescher Stopp; public void initCountandStop () {count = 1; stop = false; } public void doloop () {while (! stop) {count ++; }} public void printresult () {System.out.println (count); System.out.println (Stopp); }}Bei der Ausführung des oben genannten Code können wir denken, dass Count = 1 vor Stop = false ausgeführt wird. Dies ist im idealen Zustand korrekt, der im obigen CPU -Ausführungsdiagramm angezeigt wird, ist jedoch falsch, wenn Sie das Register- und Cache -Pufferung berücksichtigen. Zum Beispiel steht der Stopp selbst im Cache, aber die Anzahl ist nicht vorhanden, dann kann der Stopp aktualisiert werden und der Grafen -Schreibpuffer wird vor dem Zurückschreiben aktualisiert.
Darüber hinaus kann die CPU und der Compiler (normalerweise auf JIT für Java beziehen) die Anweisungsausführungsreihenfolge ändern. Zum Beispiel haben Count = 1 und Stop = False keine Abhängigkeiten, sodass die CPU und der Compiler die Reihenfolge dieser beiden ändern können. Nach Ansicht eines einsthread-Programms ist das Ergebnis das gleiche. Dies ist auch das AS-IF-SERIAL, das die CPU und der Compiler sicherstellen müssen (unabhängig davon, wie die Ausführungsreihenfolge geändert wird, bleibt das Ausführungsergebnis des einzelnen Threads unverändert). Da der größte Teil der Programmausführung Single-Threaded ist, ist eine solche Optimierung akzeptabel und führt zu großen Leistungsverbesserungen. Im Fall von Multithreading können jedoch unerwartete Ergebnisse ohne die erforderlichen Synchronisationsoperationen auftreten. Nachdem Thread T1 die InitCountandStop -Methode ausgeführt hat, führt Thread T2 Printresult aus, das 0, falsch, 1, falsch oder 0, wahr sein kann. Wenn Thread T1 doloop () zuerst und Thread T2 eine Sekunde ausführt, kann T1 aus der Schleife springen, oder es wird möglicherweise nie die Änderung des Stopps aufgrund der Compiler -Optimierung angezeigt.
Aufgrund der verschiedenen Probleme in den oben genannten Multi-Threading-Situationen ist die Programmsequenz bei Multi-Threading nicht mehr die Ausführungsreihenfolge und führt zum zugrunde liegenden Mechanismus. Die Programmiersprache muss den Entwicklern eine Garantie geben. In einfachen Worten ist diese Garantie, wenn die Änderung eines Threads für andere Threads sichtbar ist. Daher schlägt die Java -Sprache JavamemoryModel vor, dh das Java -Speichermodell, das die Implementierung gemäß den Konventionen dieses Modells erfordert. Java bietet Mechanismen wie flüchtig, synchronisiert und endgültig, um Entwicklern dabei zu helfen, die Richtigkeit von Multi-Thread-Programmen auf allen Prozessorplattformen zu gewährleisten.
Vor JDK1.5 hatte Javas Speichermodell ernsthafte Probleme. In dem alten Speichermodell kann beispielsweise ein Thread den Standardwert eines endgültigen Feldes nach Abschluss des Konstruktors sehen, und das Schreiben des flüchtigen Feldes kann mit dem Lese- und Schreiben des nichtflüchtigen Feldes neu bestellt werden.
In JDK1.5 wurde ein neues Speichermodell durch JSR133 vorgeschlagen, um die vorherigen Probleme zu beheben.
Neuordnung Regeln
Flüchtige und überwachen Schloss
| Ist es möglich, neu zu ordnen? | Die zweite Operation | Die zweite Operation | Die zweite Operation |
|---|---|---|---|
| Die erste Operation | Normales Lesen/normales Schreiben | Volatile Lese-/Monitoreingabe | Flüchtige Schreib-/Überwachungsausstellung |
| Normales Lesen/normales Schreiben | NEIN | ||
| Voaltile Read/Monitor eingeben | NEIN | NEIN | NEIN |
| Flüchtige Schreib-/Überwachungsausstellung | NEIN | NEIN |
Die normale Lektüre bezieht sich auf die Array-Ladung von Getfield-, Getstatic- und nichtflüchtigen Arrays, und die normale Lektüre bezieht sich auf den ArrayStore von Putfield, Putstatic und nichtflüchtigen Arrays.
Das Lesen und Schreiben von volatilen Feldern ist Getfield, Getstatic, Putfield, Putstatic.
Monitenter wird in den Synchronisationsblock oder die Synchronisationsmethode eingeben. Der Monitorexist bezieht sich auf das Verlassen des Synchronisationsblocks oder der Synchronisationsmethode.
Nein in der obigen Tabelle bezieht sich auf zwei Vorgänge, die keine Neuordnung zulassen. Zum Beispiel (normales Schreiben, flüchtiges Schreiben) bezieht sich auf die Neuordnung nichtflüchtiger Felder und die Neuordnung von Schriften aller nachfolgenden flüchtigen Felder. Wenn es kein Nein gibt, bedeutet dies, dass das Neubestehen zulässig ist, aber der JVM muss die minimale Sicherheit sicherstellen - der Lesewert ist entweder der Standardwert oder der von anderen Threads geschrieben (64 -Bit -Doppel- und lange Lesen- und Schreibvorgänge sind ein spezieller Fall. Wenn keine volatile Änderung vorhanden ist, wird nicht garantiert, dass das Lesen und Schreiben atomisch ist und die unterliegende Layer in zwei separate Operationen auf zwei separate Operationen aufgeteilt werden kann.
Endfeld
Es gibt zwei zusätzliche Sonderregeln für das endgültige Feld
Weder das Schreiben des endgültigen Feldes (im Konstruktor) noch das Schreiben der Referenz des endgültigen Feldobjekts selbst können mit nachfolgenden Schreibvorgängen der Objekte neu beordnet werden, die das endgültige Feld (außerhalb des Konstruktors) halten. Beispielsweise kann die folgende Erklärung nicht neu bestellt werden
X.Finalfield = v; ...; SharedRef = x;
Die erste Ladung des endgültigen Feldes kann nicht mit dem Schreiben des Objekts neu angeordnet werden, das das endgültige Feld hält. Beispielsweise erlaubt die folgende Anweisung keine Neuordnung.
x = SharedRef; ...; i = x.finalfield
Speicherbarriere
Die Prozessoren unterstützen alle bestimmte Speicherbarrieren oder Zäune, um die Sichtbarkeit von Neuordnung und Daten zwischen verschiedenen Prozessoren zu steuern. Wenn die CPU beispielsweise Daten zurückschreibt, wird die Store -Anfrage in den Schreibpuffer eingebaut und auf das Speicher in den Speicher gewartet. Diese Speicheranfrage kann verhindert werden, dass sie mit anderen Anfragen nachgeordnet werden, indem die Barriere eingefügt wird, um die Sichtbarkeit der Daten zu gewährleisten. Sie können ein Lebensbeispiel verwenden, um die Barriere zu vergleichen. Wenn Sie beispielsweise einen Hangaufzug in die U -Bahn nehmen, betreten alle nacheinander den Aufzug, aber einige Leute werden von links herumgehen, so dass die Reihenfolge beim Verlassen des Aufzugs anders ist. Wenn eine Person ein großes Gepäck blockiert (Barriere), können die Menschen dahinter nicht herumgehen :). Darüber hinaus sind die Barriere hier und die in GC verwendete Schreibbarriere unterschiedliche Konzepte.
Klassifizierung von Gedächtnisbarrieren
Fast alle Prozessoren unterstützen Barriereanweisungen eines bestimmten groben Korns, der normalerweise als Zaun (Zaun, Zaun) bezeichnet wird, um sicherzustellen, dass die vor dem Zaun eingeleiteten Ladung und Speicheranweisungen mit der Last und dem Speicher nach Zaun strikt in Ordnung sein können. Normalerweise wird es nach ihrem Zweck in die folgenden vier Arten von Hindernissen unterteilt.
Lastladungsbarrieren
Last1; Lastladung; Last2;
Stellen Sie sicher
Storestore -Barrieren
Store1; Storestore; Store2
Stellen Sie sicher, dass die Daten in Store1 für andere Prozessoren vor Store2 und danach sichtbar sind.
Laststore Barrieren
Last1; Laststore; Store2
Stellen Sie sicher, dass die Daten von Load1 vor Store2 und nach Daten spüle geladen werden
Lagervorspannungen
Store1; Korolorelad; Load2
Stellen Sie sicher, dass die Daten in Store1 vor anderen Prozessoren (z. B. dem Speicher des Speichers) sichtbar sind, bevor die Daten in Load2 und nach der Last geladen werden. Die Storeload -Barriere verhindert, dass Last eher von anderen Prozessoren geschriebene alte Daten als von kürzlich verfassten Daten gelesen wird.
Fast alle Multiprozessoren in der modernen Zeit erfordern eine Lagerung. Der Overhead der Kaaload ist normalerweise das größte, und die Lagerung hat den Einfluss von drei weiteren Barrieren, sodass die Lagerung als allgemeine (aber höhere Overhead -Barriere) verwendet werden kann.
Daher kann die oben genannte Speicherbarriere in der oben genannten Tabelle implementiert werden
| Brauchen Barrieren | Die zweite Operation | Die zweite Operation | Die zweite Operation | Die zweite Operation |
|---|---|---|---|---|
| Die erste Operation | Normales Lesen | Normales Schreiben | Volatile Lese-/Monitoreingabe | Flüchtige Schreib-/Überwachungsausstellung |
| Normales Lesen | Laststore | |||
| Normales Lesen | Storestore | |||
| Voaltile Read/Monitor eingeben | Laden | Laststore | Laden | Laststore |
| Flüchtige Schreib-/Überwachungsausstellung | Lageroad | Storestore |
Um die Regeln der endgültigen Felder zu unterstützen
X.Finalfield = v; Storestore; SharedRef = x;
Speicherbarriere einfügen
Basierend auf den obigen Regeln können Sie der Verarbeitung von flüchtigen Feldern und synchronisierten Schlüsselwörtern eine Barriere hinzufügen, um die Regeln des Speichermodells zu erfüllen.
Fügen Sie das Storestore vor der volatilen Store -Barriere ein, nachdem alle endgültigen Felder geschrieben wurden, aber das Storestore einfügen, bevor der Konstruktor zurückkehrt
Fügen Sie die Kaaload -Barriere nach dem flüchtigen Laden ein. Fügen Sie die Lastbelastung ein und ladenstore Barriere nach flüchtiger Belastung.
Der Monitor -Eingabetaste und die volatilen Lastregeln sind konsistent, und die Übergabebegeln von Monitor und volatilen Speicher sind konsistent.
Voran
Die verschiedenen oben genannten Gedächtnisbarrieren sind für Entwickler noch relativ komplex, sodass JMM eine Reihe von Regeln für teilweise Ordnung Beziehungen von Ereignissen verwenden kann, bevor es zu veranschaulichen ist. Um sicherzustellen, dass der Thread, der Operation B ausführt, das Ergebnis von Operation A sieht (unabhängig davon, ob A und B im selben Thread ausgeführt werden), muss die vorangegangene Beziehung zwischen A und B eingehalten werden, andernfalls kann das JVM sie willkürlich neu ordnen.
Vor der Regelliste
HappendBefore -Regeln umfassen
Programmsequenzregeln: Wenn der Betrieb A im Programm vor dem Vorgang B vor Operation B vorliegt, führt der Betrieb A im selben Thread die Überwachungssperrregeln vor, bevor Vor Operation B: Der Sperrvorgang in der Monitorschloss muss vor dem Sperrvorgang in derselben Monitor -Sperre durchgeführt werden.
Flüchtige Variablenregeln: Der Schreibbetrieb der flüchtigen Variablen muss Thread -Startregeln vor dem Lesevorgang der Variablen ausführen: Der Aufruf zum Thread. Start auf dem Thread muss Thread -End -Regeln vor einem Operation im Thread ausführen Vor Operation B wird vor Operation C ausgeführt, dann wird der Betrieb A vor Operation C ausgeführt.
Die Anzeigeschloss hat die gleiche Speichersemantik wie die Monitor -Sperre, und die Atomvariable hat die gleiche Speichersemantik wie die flüchtige Weise. Die Akquisition und Veröffentlichung von Sperren, die Lese- und Schreibvorgänge von volatilen Variablen erfüllen die Beziehung in voller Ordnung, sodass das Schreiben von Flüchtigkeiten vor dem nachfolgenden flüchtigen Lesungen durchgeführt werden kann.
Das oben genannte Ereignis kann mit mehreren Regeln kombiniert werden.
Nach dem Eingeben von Thread A ist beispielsweise die Operation vor der Veröffentlichung der Monitor -Sperre auf den Programmesequenzregeln basiert, und der Vorgang der Monitor -Release wird vorgestellt
Zusammenfassen
Das obige ist die detaillierte Erklärung des Java -Speichermodells JMM in diesem Artikel. Ich hoffe, es wird für alle hilfreich sein. Wenn es Mängel gibt, hinterlassen Sie bitte eine Nachricht, um darauf hinzuweisen. Vielen Dank an Freunde für Ihre Unterstützung für diese Seite!