Java gleichzeitige Programmierserie [unvollendet]:
• Java -Parallelitätsprogrammierung: Kerntheorie
• Java -gleichzeitige Programmierung: Synchronisierte und seine Implementierungsprinzipien
• Java gleichzeitige Programmierung: Synchronisierte zugrunde liegende Optimierung (Leichtes Schloss, vorgespannter Schloss)
• Java -gleichzeitige Programmierung: Zusammenarbeit zwischen Threads (Warten/Benachrichtigen/Schlaf/Ertrag/Join)
• Java gleichzeitige Programmierung: Die Verwendung von flüchtigem und seiner Prinzipien
1. Die Rolle des volatilen
In dem Artikel "Java -Parallelitätsprogrammierung: Kerntheorie" haben wir die Probleme von Sichtbarkeit, Ordnung und Atomizität erwähnt. Normalerweise können wir diese Probleme durch das synchronisierte Schlüsselwort lösen. Wenn Sie jedoch ein Verständnis des synchronisierten Prinzips haben, sollten Sie wissen, dass synchronisiert ein relativ Schwergewichtsbetrieb ist und einen relativ großen Einfluss auf die Leistung des Systems hat. Wenn es andere Lösungen gibt, vermeiden wir daher normalerweise die Verwendung synchronisiert, um das Problem zu lösen. Das volatile Schlüsselwort ist eine weitere Lösung in Java, um die Probleme der Sichtbarkeit und Ordnung zu lösen. In Bezug auf die Atomizität ist es auch ein Punkt, dass jeder zu Missverständnissen neigt: Eine einzelne Les-/Schreibbetriebsbetrieb von volatilen Variablen kann die Atomizität sicherstellen, z. B. lange und doppelte Variablen, aber es kann die Atomizität von i ++ - Operationen nicht garantieren, da ich im Wesentlichen zweimal gelesen und schreibt.
2. Verwendung von volatilen Gebrauch
In Bezug auf die Verwendung von volatilen können wir mehrere Beispiele verwenden, um deren Verwendung und Szenarien zu veranschaulichen.
1. Nachbestellung verhindern
Lassen Sie uns das Nachbestellungsproblem von einem der klassischsten Beispiele analysieren. Jeder sollte mit der Implementierung des Singleton -Modells vertraut sein, und in einer gleichzeitigen Umgebung können wir in der Regel die DCL -Methode (Double Check Locking) verwenden, um es zu implementieren. Der Quellcode lautet wie folgt:
Paket com.paddx.test.concurrent; öffentliche Klasse Singleton {public static volatile Singleton Singleton; / *** Der Konstruktor ist privat und verbietet eine externe Instanziierung*/ privat Singleton () {}; public static singleton getInstance () {if (Singleton == null) {synchronized (Singleton) {if (Singleton == null) {Singleton = new Singleton (); }} return Singleton; }}Lassen Sie uns nun analysieren, warum wir das flüchtige Schlüsselwort zwischen dem variablen Singleton hinzufügen müssen. Um dieses Problem zu verstehen, müssen Sie zunächst den Objektkonstruktionsprozess verstehen. Das Instanziieren eines Objekts kann tatsächlich in drei Schritte unterteilt werden:
(1) Speicherplatz zuweisen.
(2) Initialisieren Sie das Objekt.
(3) Weisen Sie der entsprechenden Referenz die Adresse des Speicherraums zu.
Da das Betriebssystem jedoch Anweisungen neu ordnen kann, kann der obige Vorgang auch zum folgenden Prozess werden:
(1) Speicherplatz zuweisen.
(2) Weisen Sie der entsprechenden Referenz die Adresse des Speicherraums zu.
(3) Initialisieren Sie das Objekt
Wenn dieser Prozess der Prozess ist, kann eine nicht initialisierte Objektreferenz in einer Umgebung mit mehreren Threaden aufgedeckt werden, was zu unvorhersehbaren Ergebnissen führt. Um die Neuordnung dieses Prozesses zu verhindern, müssen wir daher die Variable auf eine Variable des volatilen Typs einstellen.
2. Sichtbarkeit erreichen
Das Sichtbarkeitsproblem bezieht sich hauptsächlich auf einen Thread, der den freigegebenen variablen Wert ändert, während der andere Thread ihn nicht sehen kann. Der Hauptgrund für das Sichtbarkeitsproblem ist, dass jeder Thread seinen eigenen Cache -Bereich hat - Faden -Arbeitsspeicher. Das volatile Schlüsselwort kann dieses Problem effektiv lösen. Schauen wir uns die folgenden Beispiele an, um die Funktion zu kennen:
Paket com.paddx.test.concurrent; öffentliche Klasse volatiletest {int a = 1; int b = 2; public void change () {a = 3; B = a; } public void print () {System.out.println ("b ="+b+"; a ="+a); } public static void main (String [] args) {while (true) {endgültig volatiletest test = new volatiletest (); neuer Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printstacktrace ();} test.change ();}}). start (); neuer Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printstacktrace ();} test.print ();}}). start (); }}}Intuitiv gesehen gibt es nur zwei mögliche Ergebnisse für diesen Code: b = 3; a = 3 oder b = 2; a = 1. Wenn Sie jedoch den obigen Code ausführen (möglicherweise etwas länger dauert) werden Sie feststellen, dass zusätzlich zu den beiden vorherigen Ergebnissen auch ein drittes Ergebnis vorhanden ist:
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ...
Warum erscheint ein Ergebnis wie B = 3; a = 1? Wenn Sie zuerst die Änderungsmethode ausführen und dann die Druckmethode ausführen, sollte das Ausgabeergebnis B = 3; a = 3 ausführen. Im Gegenteil, wenn Sie zuerst die Druckmethode ausführen und dann die Änderungsmethode ausführen, sollte das Ergebnis b = 2; a = 1 sein. Wie kommt das Ergebnis von B = 3; A = 1 heraus? Der Grund dafür ist, dass der erste Thread den Wert a = 3 verändert, aber für den zweiten Thread unsichtbar ist, sodass dieses Ergebnis auftritt. Wenn sowohl A als auch B in Variablen des flüchtigen Typs geändert und ausgeführt werden, wird das Ergebnis von B = 3; A = 1 nie wieder erscheint.
3. Sorte Atomicity
Die Frage der Atomizität wurde oben erklärt. Volatile kann nur die Atomizität für einzelne Lesen/Schreiben garantieren. Dieses Problem kann in JLS beschrieben werden:
17.7 Nicht -atomarische Behandlung von Doppel und lang für die Zwecke des Java-Programmiersprachengedächtnismodells wird ein einzelner Schreiben in ein nichtflüchtiges langes oder doppeltes Wert als zwei separate Schreibvorgänge behandelt: eins jeweils 32-Bit-Hälfte. Dies kann zu einer Situation führen, in der ein Thread die ersten 32 Bit eines 64-Bit-Werts von einem Schreiben und die zweiten 32 Bit aus einem anderen Schreiben sehen. Schreibvorgänge an und Reads von Referenzen sind immer atomar, unabhängig davon, ob sie als 32-Bit- oder 64-Bit-Werte implementiert sind. Einige Implementierungen finden es möglicherweise bequem, eine einzelne Schreibaktion auf einen 64-Bit-Langen oder Doppelwert in zwei Schreibaktionen auf benachbarten 32-Bit-Werten zu unterteilen. Aus Gründen der Effizienz ist dieses Verhalten implementierungsspezifisch. Eine Implementierung der Java -Virtual Machine ist frei, Schreibvorgänge zu langen und doppelten Werten atomisch oder in zwei Teilen durchzuführen. Implementierungen der virtuellen Java-Maschine werden ermutigt, nach Möglichkeit 64-Bit-Werte zu vermeiden. Programmierer werden aufgefordert, gemeinsame 64-Bit-Werte als volatil zu deklarieren oder ihre Programme korrekt zu synchronisieren, um mögliche Kompplikationen zu vermeiden.
Der Inhalt dieser Passage ähnelt ungefähr dem, was ich zuvor beschrieben habe. Da die Operationen der beiden Datentypen von langem und doppelt in zwei Teile unterteilt werden können: hohe 32 Bit und niedrige 32 Bit, sind gewöhnliche lange oder doppelte Typen möglicherweise nicht atomar. Daher wird jeder ermutigt, die gemeinsam genutzten langen und doppelten Variablen auf volatile Typen zu setzen, die sicherstellen können, dass einzelne Lese-/Schreibvorgänge von Long und Double in jedem Fall atomar sind.
Es gibt ein Problem, dass volatile Variablen Atomizität garantieren, was leicht missverstanden zu werden. Jetzt werden wir dieses Problem durch das folgende Programm demonstrieren:
paket com.paddx.test.concurrent; public class volatiletest01 {volatile int i; public void addi () {i ++; } public static void main (String [] args) löst InterruptedException aus {endgültig volatiletest01 test01 = new Volatiletest01 (); für (int n = 0; n <1000; n ++) {neuer Thread (neuer Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {E.printstacktrace ();} Test01.addi ();}}). } Thread.sleep (10000); // Warten Sie 10 Sekunden lang, um sicherzustellen, dass die obige Programmausführung abgeschlossen ist. }}Sie können fälschlicherweise glauben, dass dieses Programm nach dem Hinzufügen des Keywords zur Variablen I Thread-Safe ist. Sie können versuchen, das obige Programm auszuführen. Hier sind die Ergebnisse meines lokalen Laufs:
Vielleicht führt jeder die Ergebnisse anders aus. Es ist jedoch zu erkennen, dass volatile Atomizität nicht garantieren kann (ansonsten sollte das Ergebnis 1000 betragen). Der Grund ist auch sehr einfach. I ++ ist tatsächlich ein zusammengesetzter Vorgang, einschließlich drei Schritten:
(1) Lesen Sie den Wert von i.
(2) 1 zu i.
(3) Schreiben Sie den Wert von I zurück in den Speicher.
Es gibt keine Garantie dafür, dass diese drei Operationen atomar sind. Wir können die Atomizität von +1 Operationen durch Atomicinderer oder synchronisiert sicherstellen.
Hinweis: Die Methode von Thread.Sleep () wurde an vielen Stellen in den obigen Codeabschnitten ausgeführt, um die Wahrscheinlichkeit von Parallelitätsproblemen zu erhöhen und keinen anderen Effekt zu haben.
3. Das Prinzip des volatilen
In den obigen Beispielen sollten wir im Grunde wissen, was flüchtig ist und wie es verwendet werden soll. Schauen wir uns nun an, wie die zugrunde liegende Schicht des flüchtigen Umfangs implementiert wird.
1. Implementierung der Sichtbarkeit:
Wie im vorherigen Artikel erwähnt, interagiert der Thread selbst nicht direkt mit den Hauptspeicherdaten, sondern vervollständigt die entsprechenden Operationen über den Arbeitsspeicher des Threads. Dies ist auch der wesentliche Grund, warum Daten zwischen Threads unsichtbar sind. Um die Sichtbarkeit volatiler Variablen zu erreichen, können Sie daher direkt von diesem Aspekt beginnen. Es gibt zwei Hauptunterschiede zwischen Schreibvorgängen zu volatilen Variablen und gewöhnlichen Variablen:
(1) Beim Ändern der volatilen Variablen wird der geänderte Wert gezwungen, den Hauptspeicher zu aktualisieren.
(2) Das Ändern der volatilen Variablen führt dazu, dass die entsprechenden Variablenwerte im Arbeitsspeicher anderer Threads fehlschlagen. Wenn Sie den Wert dieser Variablen erneut lesen, müssen Sie den Wert im Hauptspeicher erneut lesen.
Durch diese beiden Operationen kann das Sichtbarkeitsproblem der volatilen Variablen gelöst werden.
2. Ordentliche Implementierung:
Bevor Sie dieses Problem erläutern, verstehen wir zunächst die vorangegangenen Regeln in Java. Die Definition von Ereignis in JSR 133 lautet wie folgt:
Zwei Aktionen können durch eine passiert, vor der Beziehung. Wenn eine Aktion vor einer anderen stattfindet, ist die erste vor dem zweiten sichtbar und bestellt.
In Laiengesicht, wenn a vor B vor B, sind alle Operationen A für b sichtbar. (Jeder muss sich daran erinnern, weil das Wort passiert ist, bevor es leicht missverstanden wird wie vor und nach der Zeit). Schauen wir uns an, welche Vorschriften in JSR 133 definiert sind:
• Jede Aktion in einem Thread erfolgt vor jeder nachfolgenden Aktion in diesem Thread. • Ein Entsperren eines Monitors erfolgt vor jeder nachfolgenden Sperre dieses Monitors. • Ein Schreiben in ein flüchtiges Feld erfolgt vor jeder nachfolgenden Lektüre dieses volatilen. • Ein Anruf zum Starten () in einem Thread erfolgt vor Aktionen im begonnenen Thread. • Alle Aktionen in einem Thread erfolgen vor einem anderen Thread erfolgreich aus einem Join () in diesem Thread. • Wenn eine Aktion A vor einer Aktion B und B vor einer Aktion C erfolgt, findet A vor c statt.
Übersetzt als:
• Die vorherige Operation ereignet sich vor demselben Thread. (d. H. In einem einzigen Thread ist es legal, in Code -Reihenfolge auszuführen. Der Compiler und der Prozessor können jedoch neu ordnen, ohne die Ausführung zu einer einzigen Thread -Umgebung zu beeinflussen. Mit anderen Worten, dass Regeln die Zusammenstellung und Neuordnung der Kompilierung und Anweisungen nicht garantieren können).
• Entsperren Sie den Betrieb am Monitor vor dem anschließenden Sperrvorgang. (Synchronisierte Regeln)
• Schreiben Sie den Vorgang in die volatile Variable, vor dem nachfolgenden Lesevorgängen. (flüchtige Regeln)
• Die Start () -Methode des Threads passiert vor allen nachfolgenden Operationen des Threads. (Thread -Startregel)
• Alle Vorgänge des Threads, bevor andere Threads in diesem Thread zusammenarbeiten und den erfolgreichen Vorgang zurückgeben.
• Wenn a vor B, B, vor C passiert, dann ist a vor C (transitiv).
Hier schauen wir uns hauptsächlich die dritte Regel an: die Regeln, um die Ordnung der volatilen Variablen zu gewährleisten. In dem Artikel "Java -Parallelitätsprogrammierung: Kerntheorie" wurde erwähnt, dass die Neuordnung in den Compiler -Neubestehen und zur Neuordnung von Prozessor unterteilt ist. Um die volatile Speichersemantik zu implementieren, schränkt JMM die Neuordnung dieser beiden Arten von flüchtigen Variablen ein. Im Folgenden finden Sie die durch JMM für volatile Variablen angegebene Tabelle zur Neuordnung von Regeln:
| Kann neu ordnen | 2. Operation | |||
| 1. Operation | Normale Last Normaler Geschäft | Volatile Last | Flüchtiger Laden | |
| Normale Last Normaler Geschäft | NEIN | |||
| Volatile Last | NEIN | NEIN | NEIN | |
| Flüchtiger Laden | NEIN | NEIN | ||
3. Gedächtnisbarriere
Um eine volatile Sichtbarkeit und die Ereignis für Semantik zu implementieren. 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. Hier ist die Speicherbarriere, die erforderlich ist, um die oben genannten Regeln abzuschließen:
| Erforderliche Barrieren | 2. Operation | |||
| 1. Operation | Normale Last | Normaler Geschäft | Volatile Last | Flüchtiger Laden |
| Normale Last | Laststore | |||
| Normaler Geschäft | Storestore | |||
| Volatile Last | Laden | Laststore | Laden | Laststore |
| Flüchtiger Laden | Lageroad | Storestore | ||
(1) Ladelad -Barriere
Ausführungsreihenfolge: Load1 -> Loadload -> Load2
Stellen Sie sicher, dass LOAD2- und nachfolgende Lastanweisungen auf die von Load1 geladenen Daten zugreifen können, bevor Daten geladen werden.
(2) Storestore -Barriere
Ausführungsbestellung: Store1 -> Storestore - Store2
Stellen Sie sicher, dass die Daten des Store1 -Betriebs für andere Prozessoren vor Store2 sichtbar sind und die nachfolgenden Speicheranweisungen ausgeführt werden.
(3) Laststore -Barriere
Ausführungsreihenfolge: Load1 -> LoadStore -> Store2
Stellen Sie sicher, dass vor Store2 und nachfolgende Speicheranweisungen auf die von Load1 geladenen Daten zugegriffen werden können.
(4) Storeload -Barriere
Ausführungsreihenfolge: Store1 -> Storeload -> Load2
Stellen Sie sicher, dass die Daten von Store1 vor der Last2 und den nachfolgenden Lastanweisungen für andere Prozessoren sichtbar sind.
Schließlich kann ich ein Beispiel verwenden, um zu veranschaulichen, wie die Speicherbarriere in das JVM eingefügt wird:
paket com.paddx.test.concurrent; public class memoryBarrier {int a, b; volatile int v, u; void f () {int i, j; i = a; J = B; i = v; // load load j = u; // loadStore a = i; B = J; // storestore v = i; // storestore u = j; // storeload i = u; // laden // ladenstore j = b; a = i; }}4. Zusammenfassung
Insgesamt ist das Verständnis volatiles noch relativ schwierig. Wenn Sie es nicht besonders gut verstehen, müssen Sie sich nicht beeilen. Es braucht einen Prozess, um es vollständig zu verstehen. Sie werden auch die Nutzungsszenarien von flüchtigem Volatil in nachfolgenden Artikeln oft sehen. Hier habe ich ein grundlegendes Verständnis für das Grundkenntnis von flüchtigem und ursprünglichem. Im Allgemeinen ist volatile eine Optimierung der gleichzeitigen Programmierung, die in einigen Szenarien synchronisiert wird. Flüchtige kann jedoch nicht die Synchronposition vollständig ersetzen. Nur in einigen besonderen Szenarien können volatil angewendet werden. Im Allgemeinen müssen die folgenden zwei Bedingungen gleichzeitig erfüllt sein, um die Sicherheit der Fäden in einer gleichzeitigen Umgebung zu gewährleisten:
(1) Der Schreibvorgang zu Variablen hängt nicht vom aktuellen Wert ab.
(2) Diese Variable ist in der Invariante mit anderen Variablen nicht enthalten.
Der obige Artikel über die gleichzeitige Programmierung von Java: Die Verwendung von volatilen und seine Prinzipanalyse ist der gesamte Inhalt, den ich mit Ihnen teile. Ich hoffe, Sie können Ihnen eine Referenz geben und ich hoffe, Sie können wulin.com mehr unterstützen.