Vorwort
Manchmal, wenn Sie eine Synchronisation verwenden, um ein oder zwei Instanzfelder zu lesen und zu schreiben, scheint dies zu teuer zu sein. Das volatile Schlüsselwort bietet einen lockfreien Mechanismus für den synchronen Zugriff auf die Instanzfelder. Wenn eine Domäne als volatil deklariert wird, wissen der Compiler und die virtuelle Maschine, dass die Domäne über einen anderen Thread gleichzeitig aktualisiert werden kann. Bevor wir über das volatile Schlüsselwort sprechen, müssen wir die relevanten Konzepte des Speichermodells und die drei Eigenschaften der gleichzeitigen Programmierung verstehen: Atomizität, Sichtbarkeit und Ordnung.
1. Java -Speichermodell mit Atomizität, Sichtbarkeit und Ordnung
Das Java -Speichermodell sieht vor, dass alle Variablen im Hauptspeicher existieren, und jeder Thread hat seinen eigenen Arbeitsgedächtnis. Alle Vorgänge eines Threads auf einer Variablen müssen im Arbeitsspeicher ausgeführt werden und können nicht direkt im Hauptspeicher arbeiten. Und jeder Thread kann nicht auf den Arbeitsgedächtnis anderer Threads zugreifen.
Führen Sie in Java die folgende Erklärung aus:
int i = 3;
Der Ausführungs -Thread muss zuerst die Cache -Zeile zuweisen, in der sich die Variable I in seinem eigenen Arbeits -Thread befindet, und sie dann in den Hauptspeicher schreiben. Anstatt den Wert 3 direkt in den Hauptspeicher zu schreiben.
Welche Garantien sorgt die Java -Sprache selbst für Atomizität, Sichtbarkeit und Ordnung?
Atomizität
Die Lese- und Zuordnungsvorgänge von Variablen des Basistyps sind atomare Operationen, dh diese Operationen können nicht unterbrochen und entweder ausgeführt werden oder nicht.
Schauen wir uns den folgenden Code an:
x = 10; // Anweisung 1y = x; // Anweisung 2x ++; // Anweisung 3x = x + 1; // Aussage 4
Nur Aussage 1 ist eine atomare Operation, und keine der anderen drei Aussagen sind atomare Operationen.
Anweisung 2 enthält tatsächlich 2 Operationen. Es muss zuerst den Wert von x lesen und dann den Wert von X in den Arbeitsspeicher schreiben. Obwohl die beiden Operationen des Lesens des Wertes von X und dem Schreiben des Werts von X in den Arbeitsspeicher atomare Operationen sind, sind sie nicht zusammen atomarer Operationen.
In ähnlicher Weise enthalten X ++ und X = X+1 3 Operationen: Lesen Sie den Wert von x, führen Sie den Betrieb von 1 durch und schreiben Sie den neuen Wert.
Mit anderen Worten, nur einfache Lektüre und Zuordnung (und die Nummer müssen einer Variablen zugeordnet werden, und die gegenseitige Zuordnung zwischen Variablen ist keine Atomoperation) ist eine atomare Operation.
Es gibt viele Klassen in der java.util.concurrent.atomic-Paket, die sehr effiziente Anweisungen auf maschineller Ebene (anstelle von Sperren) verwenden, um die Atomizität anderer Vorgänge sicherzustellen. Beispielsweise bietet die Atomicinder -Klasse Methoden Incrementandget und Decrementandget, die eine Ganzzahl auf atomare Weise erhöhen und verringern. Die Atomicinteger -Klasse kann sicher als gemeinsamer Zähler ohne Synchronisation verwendet werden.
Darüber hinaus enthält dieses Paket auch Atomklassen wie Atomicboolean, Atomiclong und Atomicreference für Systemprogrammierer, die nur gleichzeitige Tools entwickeln, und Anwendungsprogrammierer sollten diese Klassen nicht verwenden.
Sichtweite
Die Sichtbarkeit bezieht sich auf die Sichtbarkeit zwischen Threads, und der modifizierte Zustand eines Threads ist für einen anderen Thread sichtbar. Das ist das Ergebnis einer Fadenveränderung. Ein weiterer Thread wird sofort gesehen.
Wenn eine gemeinsam genutzte Variable durch volatile geändert wird, stellt sie sicher, dass der geänderte Wert sofort auf den Hauptspeicher aktualisiert wird, sodass er für andere Threads sichtbar ist. Wenn andere Threads lesen müssen, wird der neue Wert im Speicher gelesen.
Stammhelme gemeinsame Variablen können jedoch die Sichtbarkeit nicht garantieren, da es ungewiss ist, wann die normale gemeinsam genutzte Variable nach der Änderung in den Hauptspeicher geschrieben wird. Wenn andere Themen es lesen, kann der ursprüngliche alte Wert noch im Speicher liegen, sodass die Sichtbarkeit nicht garantiert werden kann.
Ordentlich
Im Java-Speichermodell dürfen Compiler und Prozessoren Anweisungen neu anordnen, aber der Nachbestellungsprozess wirkt sich nicht auf die Ausführung von Programmen mit Single-Thread-Programmen aus, sondern wirkt sich auf die Richtigkeit der gleichzeitigen Ausführung mit mehreren Threads aus.
Das volatile Schlüsselwort kann verwendet werden, um eine bestimmte "Bestelllinie" zu gewährleisten. Darüber hinaus kann synchronisiert und sperren verwendet werden, um die Reihenfolge sicherzustellen. Synchronisierte und Sperren stellen natürlich sicher, dass es in jedem Moment einen Thread gibt, der den Synchronisationscode ausführt, der dem Synchronisationscode in Sequenz entspricht, was natürlich die Reihenfolge gewährleistet.
2. Volatile Schlüsselwörter
Sobald eine gemeinsame Variable (Klassenmitgliedvariablen, statische Mitgliedsvariablen) durch volatile modifiziert ist, hat sie zwei Semantikschichten:
Schauen wir uns zuerst einen Code an. Wenn Thread 1 zuerst ausgeführt wird und Thread 2 später ausgeführt wird:
// Thread 1Boolean stop = false; while (! stop) {dosomething ();} // Thread 2stop = true; Viele Menschen können diese Markup -Methode verwenden, wenn sie Threads unterbrechen. Aber wird dieser Code in der Tat ganz richtig ausgeführt? Wird der Thread unterbrochen? Nicht unbedingt. Vielleicht kann dieser Code die meiste Zeit Threads unterbrechen, kann aber auch dazu führen, dass der Thread nicht unterbrochen wird (obwohl diese Möglichkeit sehr klein ist, sobald dies zu einer toten Schleife führt).
Warum ist es möglich, dass Fadenfäden unterbrechen? Jeder Thread hat seinen eigenen Arbeitsspeicher während seines laufenden Prozesses. Wenn Thread 1 ausgeführt wird, kopiert er den Wert der Stoppvariablen und stellt ihn in seinen eigenen Arbeitsspeicher. Wenn Thread 2 dann den Wert der Stoppvariablen ändert, aber keine Zeit hatte, ihn in den Hauptspeicher zu schreiben, geht Thread 2 für andere Dinge, und Thread 1 weiß nicht über die Änderungen von Thread 2 an der Stoppvariablen, sodass es weiterhin schleifen wird.
Aber nach dem Modifizieren mit volatilen wird es anders:
Garantiert volatile Atomizität?
Wir wissen, dass das volatile Schlüsselwort die Sichtbarkeit von Operationen sicherstellt, kann jedoch volatil sicherstellen, dass die Operationen für Variablen atomar sind?
public class test {public volatile inp Inc = 0; public void erhöht () {inc ++; } public static void main (string [] args) {endgültig test Test = new Test (); für (int i = 0; i <10; i ++) {neuer Thread () {public void run () {für (int j = 0; j <1000; j ++) test.increase (); }; }.Start(); } // Stellen Sie sicher, dass die vorherigen Threads die Ausführung abgeschlossen haben, während (Thread.ActiveCount ()> 1) Thread.yield (); System.out.println (test.inc); }} Das Ergebnis dieses Codes ist jedes Mal inkonsistent, wenn er ausgeführt wird. Es ist eine Zahl von weniger als 10.000. Wie bereits erwähnt, ist der automatische Inkrementbetrieb nicht atomar. Es enthält das Lesen des ursprünglichen Wertes einer Variablen, die Durchführung einer zusätzlichen 1 -Operation und das Schreiben in den Arbeitsspeicher. Das heißt, die drei Suboperationen des Selbstunternehmens können separat ausgeführt werden.
Wenn der Wert von Variable Inc zu einem bestimmten Zeitpunkt 10 beträgt, führt Thread 1 einen Selbstversorgungoperium für die Variable durch, Thread 1 liest zuerst den ursprünglichen Wert von Variable Inc und dann wird Thread 1 blockiert. Anschließend führt Thread 2 einen Selbstunternehmensvorgang in der Variablen aus, und Thread 2 liest auch den ursprünglichen Wert von Variable Inc. Da Thread 1 nur einen Lesevorgang auf der Variablen -Inc ausführt und die Variable nicht ändert, wird die Cache -Zeile von Cache Variable Inc in Thread 2 im Arbeitsspeicher nicht ungültig sein, sodass Thread 2 direkt zum Hauptspeicher gelesen wird, um den Wert von Inc zu lesen. Wenn sich herausstellt, dass der Wert von INC 10 beträgt, dann eine Erhöhung von 1 durchführt und 11 in das Arbeitsgedächtnis schreibt und schließlich in das Hauptgedächtnis schreibt. Dann führt Faden 1 dann den Additionsvorgang durch. Da der Wert von INC gelesen wurde, beachten Sie, dass der Wert von Inc in Thread 1 zu diesem Zeitpunkt immer noch 10 beträgt. Nachdem Thread 1 Inc hinzugefügt wurde, beträgt der Wert von INC 11, schreibt dann 11 in die Arbeit und schreibt ihn schließlich zum Hauptspeicher. Nachdem die beiden Threads eine Selbststillstandsoperation durchgeführt haben, erhöht sich Inc nur um 1.
Der Autoinkrementbetrieb ist kein Atombetrieb, und flüchtig kann nicht garantieren, dass ein Betrieb auf einer Variablen atomar ist.
Kann volatile Ordnung sicherstellen?
Wie bereits erwähnt, kann das volatile Schlüsselwort die Neuordnung der Anweisungen untersagen, sodass flüchtig in gewissem Maße die Ordnung sicherstellen kann.
Es gibt zwei Bedeutungen, die die Nachbestellung von volatilen Schlüsselwörtern verboten haben:
3. Verwenden Sie das volatile Schlüsselwort richtig
Das synchronisierte Schlüsselwort verhindert, dass mehrere Threads gleichzeitig einen Code ausführen, was die Programmausführungseffizienz stark beeinflusst. Die Leistung des volatilen Schlüsselworts ist in einigen Fällen besser als synchronisiert. Es ist jedoch zu beachten, dass das volatile Schlüsselwort das synchronisierte Schlüsselwort nicht ersetzen kann, da das volatile Schlüsselwort die Atomizität des Betriebs nicht garantieren kann. Im Allgemeinen müssen die folgenden zwei Bedingungen bei der Verwendung von volatilen Erkrankungen erfüllt sein:
Die erste Bedingung ist, dass es sich nicht um Operationen wie Selbstverlagerung und Selbstverstärkung handeln kann. Wie oben erwähnt, garantiert volatile Atomizität nicht.
Lassen Sie uns ein Beispiel dafür geben. Es enthält eine Invariante: Die untere Grenze ist immer geringer als oder gleich der Obergrenze.
öffentliche Klassennummernrange {private volatile int untere, obere; public int getlower () {return niedriger; } public int getupper () {return ober; } public void setlower (int value) {if (value> obere) werfen neue illegalArgumentException (...); niedrigerer = Wert; } public void setupper (int value) {if (value <niedriger) neue illegalArgumentException (...); ober = Wert; }} Auf diese Weise begrenzt die Variablen der Scoped -Status und das Definieren der unteren und oberen Felder als flüchtige Typen implementiert die Gewindesicherheit der Klasse nicht vollständig und erfordert daher immer noch eine Synchronisation. Andernfalls ist der Bereich inkonsistent, wenn zwei Threads Setlower und Setupper gleichzeitig mit inkonsistenten Werten ausführen. Wenn der Ausgangszustand beispielsweise (0, 5) ist, gleichzeitig der Thread A-Aufrufsetlower (4) und Thread B Setupper (3), ist es offensichtlich, dass die in der von diesen beiden Operationen gespeicherten Werte nicht den Bedingungen erfüllen, dann werden beide Threads den Scheck übergeben, um den Invariant zu schützen, sodass der letzte Reichweite Wert ist (4, 3), was offensichtlich falsch ist.
In der Tat soll die Atomizität des Vorgangs sanft, um volatil zu verwenden. Es gibt zwei Hauptszenarien für die Verwendung von volatilen:
Statusflaggen
volatile boolean schaltdownRequested; ... public void stilldown () {stilldownRequested = true; } public void dowork () {while (! stilldownRequested) {// do Sachen}}Es ist wahrscheinlich, dass die Shutdown () -Methode von außerhalb der Schleife aufgerufen wird - d. H. In einem anderen Thread -, sodass eine Art von Synchronisation durchgeführt werden muss, um sicherzustellen, dass die Sichtbarkeit der Variablen zum ShutdownRequested korrekt implementiert ist. Das Schreiben einer Schleife mit synchronisierten Blöcken ist jedoch viel mehr problematisch als das Schreiben mit volatilen Statusflaggen. Da volatile die Codierung vereinfacht und die Statusflags nicht von einem anderen Zustand des Programms abhängen, ist es hier sehr geeignet für volatile.
Double Check -Modus (DCL)
öffentliche Klasse Singleton {private volatile statische Singleton Instance = NULL; public static singleton getInstance () {if (instance == null) {synchronized (this) {if (instance == null) {instance = new Singleton (); }} return Instance; }} Die Verwendung von volatilen hier wirkt sich mehr oder weniger auf die Leistung aus, aber unter Berücksichtigung der Richtigkeit des Programms lohnt es sich immer noch, diese Leistung zu opfern.
Der Vorteil von DCL besteht darin, dass sie eine hohe Ressourcenauslastung aufweist. Singleton -Objekte werden nur dann instanziiert, wenn der GetInstance zum ersten Mal ausgeführt wird, was sehr effizient ist. Der Nachteil ist, dass die Reaktion beim Erstladen beim Laden etwas langsamer ist, und es gibt bestimmte Defekte in hohen Parallelitätsumgebungen, obwohl die Wahrscheinlichkeit des Auftretens sehr gering ist.
Obwohl DCL die Probleme des Ressourcenverbrauchs, unnötiger Synchronisation, Gewindesicherheit usw. in gewissem Maße löst, hat es in einigen Fällen immer noch Fehlerprobleme, dh DCL -Fehler. In dem Buch "Java Concurrency Programing Practice" wird die Verwendung des folgenden Code (statische interne Klassen -Singleton -Muster) zum Ersetzen von DCL empfohlen:
öffentliche Klasse Singleton {private Singleton () {} public static Singleton getInstance () {return Singletonholder.sinstance; } private statische Klasse Singletonholder {private statische Finale Singleton Sinstance = New Singleton (); }} Über Doppelprüfungen können Sie anzeigen
4. Zusammenfassung
Im Vergleich zu Schlössern ist die volatile Variable sehr einfach, aber gleichzeitig sehr fragile Synchronisationsmechanismus, der in einigen Fällen eine bessere Leistung und Skalierbarkeit bietet als Schlösser. Wenn Sie strikt den Nutzungsbedingungen von Flüchtigkeiten befolgen, die wirklich unabhängig von anderen Variablen und ihren eigenen früheren Werten sind, können Sie volatil anstatt synchronisiert zu verwenden, um den Code in einigen Fällen zu vereinfachen. Der Code mit volatilen Mitarbeitern ist jedoch häufig anfälliger für Fehler als Code mit Sperren. In diesem Artikel werden zwei häufigste Anwendungsfälle eingeführt, in denen flüchtig anstelle von synchronisiert werden kann. In anderen Fällen sollten wir besser synchronisiert werden.
Das Obige dreht sich alles um diesen Artikel, ich hoffe, es wird für das Lernen aller hilfreich sein.