Vorwort
Der vorherige Artikel sprach über das CAS -Prinzip, in dem die Atomic* -Klasse erwähnt wurde. Der Mechanismus zur Implementierung von Atomoperationen beruht auf den Merkmalen der Speichersichtbarkeit von flüchtigem. Wenn Sie CAS und Atomic* noch nicht kennen, wird empfohlen, sich anzutragen, über das Cas Spin Lock sprechen.
Drei Eigenschaften der Parallelität
Erstens, wenn wir volatile verwenden wollen, muss es in einer Mehrwertverkehrsumgebung mit mehreren Threaden enthalten sein. Es gibt drei wichtige Merkmale im gleichzeitigen Szenario, über das wir oft sprechen: Atomizität, Sichtbarkeit und Ordnung. Nur wenn diese drei Merkmale erfüllt sind, kann das gleichzeitige Programm korrekt ausgeführt werden, sonst treten verschiedene Probleme auf.
Atomizität, die im vorherigen Artikel erwähnten CAS- und Atomic* -Klasse können die Atomizität einfacher Operationen sicherstellen. Für einige verantwortungsvolle Operationen kann es mit synchronisierten oder verschiedenen Schlössern implementiert werden.
Die Sichtbarkeit bezieht sich darauf, wenn mehrere Threads auf dieselbe Variable zugreifen, ein Thread den Wert der Variablen verändert und andere Threads den geänderten Wert sofort sehen können.
Die Reihenfolge der Programmausführung wird in der Reihenfolge des Codes ausgeführt, und Anweisungen sind verboten, neu zu bestellen. Es scheint natürlich, dass dies nicht der Fall ist. Die Neuordnung der Anweisungen ist die JVM, um die Anweisungen zu optimieren und die Effizienz des Programmbetriebs zu verbessern und die Parallelität so weit wie möglich zu verbessern, ohne die Ausführungsergebnisse eines einzel-thread-Programms zu beeinträchtigen. In einer Umgebung mit mehreren Threaden kann die Reihenfolge einiger Codes jedoch logische Falschheit verursachen.
Volatile implementiert zwei Merkmale, Sichtbarkeit und Reihenfolge. Daher ist es in einer Umgebung mit mehreren Threaden erforderlich, die Funktion dieser beiden Merkmale zu gewährleisten, und das flüchtige Schlüsselwort kann verwendet werden.
Wie volatil die Sichtbarkeit garantiert
Wenn es um Sichtbarkeit geht, müssen Sie den Prozessor und den Hauptspeicher des Computers verstehen. Aufgrund des Multi-Threading wird es schließlich in einem Computerprozessor durchgeführt, egal wie viele Themen es gibt. Die heutigen Computer sind im Grunde genommen Multi-Kern, und einige Maschinen haben sogar Multi-Prozessoren. Schauen wir uns das Strukturdiagramm eines Multiprozessors an:
Dies ist eine CPU mit zwei Prozessoren, einem Quad-Core. Ein Prozessor entspricht einem physischen Steckplatz, und mehrere Prozessoren werden über einen QPI -Bus verbunden. Ein Prozessor besteht aus mehreren Kernen und einem gemeinsam genutzten L3-Cache zwischen den Prozessoren. Ein Kern enthält Register, L1 -Cache, L2 -Cache.
Während der Ausführung des Programms müssen das Lesen und Schreiben von Daten beteiligt sein. Wir alle wissen, dass die Speicherzugriffsgeschwindigkeit, obwohl sie bereits sehr schnell ist, die Geschwindigkeit der CPU -Ausführung von Anweisungen immer noch weit unterlegen ist. Daher werden im Kernel, L1, L2 und L3 Level Drei Caches hinzugefügt. Wenn das Programm ausgeführt wird, werden die erforderlichen Daten zuerst vom Hauptspeicher zum Cache des Kerns kopiert und nach Abschluss des Vorgangs in den Hauptspeicher geschrieben. Die folgende Abbildung ist ein schematisches Diagramm des CPU -Zugriffs auf Daten, von Registern über Cache bis hin zu Hauptspeichern und sogar Festplatten. Die Geschwindigkeit wird immer langsamer.
Lassen Sie uns nach dem Verständnis der CPU-Struktur einen Blick auf den spezifischen Prozess der Programmausführung betrachten und als Beispiel einen einfachen Selbstaufbauoperation machen.
i = i+1;
Bei der Ausführung dieser Anweisung kopiert ein Thread, das auf einem Kern ausgeführt wird, den Wert von I zum Cache, in dem sich der Kern befindet. Nach Abschluss der Operation wird sie in den Hauptspeicher zurückgeschrieben. In einer Umgebung mit mehreren Threads verfügt jeder Thread über einen entsprechenden Arbeitsspeicher im Cache-Bereich im laufenden Kern, dh jeder Thread verfügt über einen eigenen privaten Arbeitscache-Bereich, um die für den Vorgang erforderlichen Replikaten zu speichern. Schauen wir uns dann das Problem von i+1 an. Unter der Annahme, dass der Anfangswert von I 0 beträgt, gibt es zwei Threads, die diese Anweisung gleichzeitig ausführen, und jeder Thread benötigt drei Schritte, um auszuführen:
1. Lesen Sie den I -Wert aus dem Hauptspeicher zum Arbeitsspeicher von Thread, dh den entsprechenden Kernel -Cache -Bereich;
2. Berechnen Sie den Wert von i+1;
3. Schreiben Sie den Ergebniswert zurück in den Hauptspeicher;
Nachdem die beiden Threads jeweils 10.000 Mal ausgeführt wurden, sollte der erwartete Wert 20.000 betragen. Leider ist der Wert von I immer weniger als 20.000. Einer der Gründe für dieses Problem ist das Problem der Cache -Konsistenz. Sobald eine Cache -Kopie eines Threads geändert wurde, sollte die Cache -Kopie anderer Threads sofort ungültig werden.
Nach der Verwendung des volatilen Schlüsselworts werden die folgenden Effekte sein:
1. Jedes Mal, wenn die Variable geändert wird, wird der Prozessor -Cache (Arbeitsspeicher) in den Hauptspeicher zurückgeschrieben.
2. Das Schreiben zum Hauptspeicher eines Arbeitsspeichers führt dazu, dass der Prozessor -Cache (Arbeitsspeicher) anderer Threads ungültig ist.
Da volatile die Sichtbarkeit von Speicher sicherstellt, verwendet es tatsächlich das MESI -Protokoll, das die Cache -Konsistenz durch CPU gewährleistet. Es gibt viele Inhalte des Mesi -Protokolls, daher werde ich es hier nicht erklären. Bitte schau es dir selbst an. Kurz gesagt, das volatile Schlüsselwort wird verwendet. Wenn die Änderung eines Threads an der flüchtigen Variablen sofort in den Hauptspeicher zurückgeschrieben wird, wodurch die Cache -Zeile anderer Threads ungültig wird und andere Threads gezwungen sind, die Variable erneut zu verwenden, muss er aus dem Hauptspeicher gelesen werden.
Dann ändern wir die oben genannte I -Variable mit volatilen und führen sie erneut aus. Jeder Thread wird 10.000 Mal ausgeführt. Leider sind es immer noch weniger als 20.000. Warum ist das?
Volatile verwendet das MESI -Protokoll der CPU, um die Sichtbarkeit zu gewährleisten. Beachten Sie jedoch, dass volatile die Atomizität der Operation nicht garantiert, da dieser selbstverletzte Vorgang in drei Schritte unterteilt ist. Angenommen, Thread 1 liest den I -Wert aus dem Hauptspeicher unter der Annahme, dass er 10 ist, und zu diesem Zeitpunkt tritt eine Blockierung auf, aber ich wurde noch nicht geändert. Zu diesem Zeitpunkt liest Thread 2 auch den I -Wert aus dem Hauptspeicher. Zu diesem Zeitpunkt ist der von diesen beiden Threads gelesene I -Wert dieselbe, sowohl 10 als auch Thread 2 fügt 1 1 hinzu und schreibt es sofort zum Hauptspeicher zurück. Zu diesem Zeitpunkt wird nach dem MESI -Protokoll die Cache -Zeile, die dem Arbeitsspeicher von Thread 1 entspricht, in einen ungültigen Zustand eingestellt, ja. Bitte beachten Sie jedoch, dass Thread 1 den I -Wert bereits aus dem Hauptspeicher kopiert hat, und nun nur das Hinzufügen von 1 und das Schreiben zum Hauptspeicher. Beide Threads fügen 1 auf der Basis von 10 hinzu und schreiben dann in den Hauptspeicher zurück, sodass der Endwert des Hauptspeichers nur 11 beträgt, nicht die erwarteten 12.
Die Verwendung von volatilen kann daher die Sichtbarkeit der Speicher sicherstellen, kann jedoch keine Atomizität garantieren. Wenn noch Atomizität benötigt wird, können Sie sich auf diesen vorherigen Artikel beziehen.
Wie volatil die Ordnung sichert
Das Java -Speichermodell verfügt über eine angeborene "Orderline", dh es kann ohne Mittel garantiert werden. Dies wird normalerweise als Prinzip bezeichnet. Wenn die Ausführungsreihenfolge von zwei Operationen nicht aus dem vorderen Prinzip abgeleitet werden kann, können sie ihre Ordnung nicht garantieren und virtuelle Maschinen können sie nach Belieben neu ordnen.
Das Folgende sind 8 Prinzipien von passiert-auszuziehen aus "eingehender Verständnis der virtuellen Java-Maschinen".
Hier werden wir hauptsächlich über die Regeln des flüchtigen Schlüsselworts sprechen und ein Beispiel für die Doppelprüfung im berühmten Singleton -Muster geben:
Klasse Singleton {private volatile statische Singleton Instance = null; private Singleton () {} public static Singleton getInstance () {if (instance == null) {// Schritt 1 Synchronized (Singleton.class) {if (instance == null) // Schritt 2 Instanz = new Singleton (); // Schritt 3}} Rückgabeinstanz; }}Wenn die Instanz nicht mit flüchtigem modifiziert ist, welche Ergebnisse können erzeugt werden? Angenommen, es gibt zwei Threads, die die Methode getInstance () aufrufen. Thread 1 führt Schritt1 aus und stellt fest, dass die Instanz null ist und dann die Singleton -Klasse synchron sperrt. Dann bestimmt, ob die Instanz wieder null ist, und stellt fest, dass es immer noch null ist und dann Schritt 3 ausführt und Singleton instanziiert. Während des Instanziierungsprozesses geht Thread 2 zu Schritt 1 und kann möglicherweise feststellen, dass die Instanz nicht leer ist, aber zu diesem Zeitpunkt ist die Instanz möglicherweise nicht vollständig initialisiert.
Was bedeutet es? Das Objekt wird in drei Schritten initialisiert und durch den folgenden Pseudo-Code dargestellt:
memory = alocalced (); // 1. Den Speicherraum des Objekts Ctorinstance (Speicher) zuweisen; // 2. Initialisieren Sie die Objektinstanz = Speicher; // 3. Legen Sie den Speicherraum des Objekts ein, das auf das Objekt zeigt
Da Schritt 2 und Schritt 3 von Schritt 1 abhängen müssen und Schritt 2 und Schritt 3 keine Abhängigkeit haben, ist es möglich, dass diese beiden Aussagen eine erneute Anordnung unterzogen werden, dh oder es ist möglich, dass Schritt 3 vor Schritt 2 ausgeführt wird. In diesem Fall wurde Schritt 3 noch nicht ausgeführt. Im Moment beurteilt Thread 2, dass die Instanz nicht null ist, sodass sie die Instanzinstanz direkt zurückgibt. Zu diesem Zeitpunkt ist die Instanz jedoch tatsächlich ein unvollständiges Objekt, daher wird es bei der Verwendung Probleme geben.
Unter Verwendung des volatilen Schlüsselworts bedeutet das Prinzip "Schreiben einer durch volatilen modifizierten Variablen, die zu jeder nachfolgenden Zeit zu lesen ist" dem obigen Initialisierungsprozess. Die Schritte 2 und 3 schreiben beide Instanzen, daher müssen sie später beim Lesen von Instanzen auftreten. Das heißt, es besteht keine Möglichkeit, eine Instanz zurückzugeben, die nicht vollständig initialisiert wird.
Die zugrunde liegende JVM wird durch etwas geschehen, das als "Speicherbarriere" bezeichnet wird. Die Speicherbarriere, auch als Speicherzaun bekannt, ist eine Reihe von Prozessoranweisungen, mit denen sequentielle Beschränkungen für Speichervorgänge implementiert werden.
endlich
Durch das volatile Schlüsselwort haben wir die Sichtbarkeit und Ordnung bei der gleichzeitigen Programmierung erfahren, was natürlich nur ein einfaches Verständnis ist. Für ein tieferes Verständnis müssen Sie sich auf Ihre Klassenkameraden verlassen, um es selbst zu studieren.
Verwandte Artikel
Worüber sprechen die CAS -Spinschlösser, über die wir sprechen?
Zusammenfassen
Das obige ist der gesamte Inhalt dieses Artikels. Ich hoffe, dass der Inhalt dieses Artikels einen gewissen Referenzwert für das Studium oder die Arbeit eines jeden hat. Wenn Sie Fragen haben, können Sie eine Nachricht zur Kommunikation überlassen. Vielen Dank für Ihre Unterstützung bei Wulin.com.