Der Hauptinhalt dieses Artikels ist ein allgemeiner Wissenspunkt im Java -Interview: Volatile Keywords. In diesem Artikel werden alle Aspekte der volatilen Schlüsselwörter im Detail eingeführt. Ich hoffe, dass Sie nach dem Lesen dieses Artikels die damit verbundenen Probleme der volatilen Schlüsselwörter perfekt lösen können.
In Java-bezogenen Vorstellungsgesprächen untersuchen viele Interviewer das Verständnis des Interviewers für Java-Parallelität. Mit dem volatilen Schlüsselwort als kleiner Einstiegspunkt können Sie das Java -Speichermodell (JMM) und einige Funktionen der gleichzeitigen Programmierung von Java häufig fragen. Eingehend können Sie auch die zugrunde liegende JVM -Implementierung und das Betriebssystem in Verbindung untersuchen. Nehmen wir einen hypothetischen Interviewprozess, um ein detailliertes Verständnis für das volituile Schlüsselwort zu erlangen!
Soweit ich weiß, haben gemeinsame Variablen, die durch volatile geändert wurden, die folgenden zwei Merkmale:
1. Sicherstellen Sie die Sichtbarkeit verschiedener Threads in der variablen Operation.
2. Verbot der Befehls -Neuordnung
Das ist viel zu sprechen, also werde ich mit dem Java -Speichermodell beginnen. Die Spezifikation der Java Virtual Machine versucht, ein Java -Speichermodell (JMM) zu definieren, um Unterschiede für den Speicherzugriff zwischen verschiedenen Hardware- und Betriebssystemen auszubilden, damit Java -Programme konsistente Speicherzugriffseffekte auf verschiedenen Plattformen erzielen können. Einfach ausgedrückt, da die CPU Anweisungen sehr schnell ausführt, ist die Speicherzugriffsgeschwindigkeit viel langsamer und der Unterschied ist keine Größenordnung. Die großen Leute, die am Prozessor arbeiten, haben der CPU mehrere Cache -Schichten hinzugefügt. Im Java -Speichermodell wird die obige Optimierung erneut abstrahiert. JMM sieht vor, dass sich alle Variablen im Hauptspeicher befinden, ähnlich wie der oben erwähnte gewöhnliche Speicher, und jeder Thread enthält seinen eigenen Arbeitsspeicher. Es kann als Register oder Cache in der CPU für ein leichtes Verständnis angesehen werden. Daher basieren Thread -Operationen hauptsächlich auf dem Arbeitsspeicher. Sie können nur auf ihren eigenen Arbeitsspeicher zugreifen und müssen den Wert vor und nach der Arbeit wieder in den Hauptspeicher synchronisieren. Ich weiß nicht einmal, was ich so gesagt habe. Nehmen Sie ein Stück Papier zum Zeichnen:
Bei der Ausführung eines Threads wird der Wert der Variablen zuerst aus dem Hauptspeicher gelesen, dann in die Kopie im Arbeitsspeicher geladen und dann zur Ausführung an den Prozessor weitergegeben. Nach Abschluss der Ausführung wird der Kopie im Arbeitsspeicher ein Wert zugewiesen, und dann wird der Wert im Arbeitsspeicher an den Hauptspeicher übergeben, und der Wert im Hauptspeicher wird aktualisiert. Obwohl die Verwendung von Arbeitsspeicher und Hauptspeicher schneller ist, bringt es auch einige Probleme mit sich. Schauen Sie sich zum Beispiel das folgende Beispiel an:
i = i + 1;
Angenommen, der Anfangswert von I ist 0, wenn nur ein Thread ihn ausführt, wird das Ergebnis auf jeden Fall 1. Wenn zwei Threads ausgeführt werden, wird das Ergebnis 2? Dies ist nicht unbedingt der Fall. Dies kann der Fall sein:
Thread 1: Laden i aus dem Hauptspeicher // i = 0 i + 1 // i = 1 Thread 2: Laden i aus dem Hauptspeicher // Da Thread 1 den Wert von I zurück zum Hauptspeicher geschrieben hat
Wenn zwei Threads dem obigen Ausführungsprozess folgen, ist der letzte Wert von I tatsächlich 1. Wenn der letzte Rückschreiben langsam ist und Sie den Wert von i erneut lesen können, kann es 0 sein, was das Problem der Cache -Inkonsistenz ist. Das Folgende ist die Frage zu erwähnen, die Sie gerade gestellt haben. JMM ist hauptsächlich darüber festgelegt, wie mit den drei Eigenschaften von Atomizität, Sichtbarkeit und Ordnung im Parallelitätsprozess umgegangen werden kann. Durch die Lösung dieser drei Probleme kann das Problem der Cache -Inkonsistenz gelöst werden. Und volatil hängt mit Sichtbarkeit und Ordnung zusammen.
1. Atomizität: In Java sind die Lese- und Zuordnungsvorgänge grundlegender Datentypen atomare Operationen. Die sogenannten atomaren Operationen bedeuten, dass diese Operationen ununterbrochen sind und für einen bestimmten Zeitraum abgeschlossen werden müssen, oder sie werden nicht ausgeführt. Zum Beispiel:
i = 2; j = i; i ++; i = i+1;
Unter den oben genannten vier Operationen ist i = 2 eine Lesevorrichtung, die eine Atomoperation sein muss. J = Ich denke, es ist eine atomare Operation. Tatsächlich ist es in zwei Schritte unterteilt. Eine ist, den Wert von i zu lesen und dann J den Wert zuzuweisen. Dies ist eine 2-Stufen-Operation. Es kann nicht als Atomoperation bezeichnet werden. i ++ und i = i+ 1 sind tatsächlich gleichwertig. Lesen Sie den Wert von i, fügen Sie 1 hinzu und schreiben Sie ihn zurück in den Hauptspeicher. Das ist eine 3-Stufen-Operation. Daher kann der letzte Wert im obigen Beispiel viele Situationen haben, da er die Atomizität nicht erfüllen kann. Auf diese Weise gibt es nur einfaches Lesen. Die Zuordnung ist eine Atomoperation oder nur eine numerische Zuordnung. Wenn Sie Variablen verwenden, gibt es eine zusätzliche Operation, um den variablen Wert zu lesen. Eine Ausnahme ist, dass die Spezifikation der virtuellen Maschine 64-Bit-Datentypen (lang und doppelt) in zwei Operationen verarbeitet werden kann, aber die neueste JDK-Implementierung implementiert immer noch Atomoperationen. JMM implementiert nur grundlegende Atomizität. Operationen wie das obige I ++ müssen synchronisiert und sperren, um die Atomizität des gesamten Code zu gewährleisten. Bevor der Faden das Schloss veröffentlicht, wird der Wert von i zurück zum Hauptspeicher putzen. 2. Sichtbarkeit: Java spricht von Sichtbarkeit und verwendet volatil, um Sichtbarkeit zu gewährleisten. Wenn eine Variable durch flüchtiges geändert wird, wird die Änderung sofort zum Hauptspeicher aktualisiert. Wenn andere Threads die Variable lesen müssen, wird der neue Wert im Speicher gelesen. Dies wird nicht durch gewöhnliche Variablen garantiert. In der Tat kann synchronisiert und sperren auch die Sichtbarkeit sicherstellen. Bevor das Faden das Schloss veröffentlicht, wird alle gemeinsam genutzten Variablenwerte zum Hauptspeicher zurückgespült, aber synchronisiert und die Sperre sind teurer. 3. Die Bestellung von JMM ermöglicht es dem Compiler und dem Prozessor, Anweisungen neu zu ordnen, aber die As-If-seriale Semantik festlegt, dh unabhängig davon, wie das Nachbestehen des Programms nicht geändert werden kann. Zum Beispiel das folgende Programmsegment:
doppelt pi = 3,14; // adouble r = 1; // bDouble s = pi * r * r; // c
Die obige Anweisung kann in a-> b-> c ausgeführt werden, wobei das Ergebnis 3.14 beträgt, aber auch in der Reihenfolge von B-> a-> c ausgeführt werden kann. Da A und B zwei unabhängige Aussagen sind, kann C, während C von A und B abhängt, um neu angeordnet werden, aber C kann in A und B. JMM nicht zuerst eingestuft werden, um sicherzustellen, dass die Neugestaltung die Ausführung eines einzelnen Threads nicht beeinträchtigt, aber Probleme sind anfällig, wenn sie bei Multi-Threading auftreten. Zum Beispiel wie dieser Code:
int a = 0; bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Wenn zwei Threads das obige Code -Segment ausführen, fügt Thread 1 zuerst Schreiben aus, dann Thread 2, dann multiplizieren Sie, Muss der Wert von RET 4 sein? Das Ergebnis ist nicht unbedingt:
Wie in der Abbildung gezeigt, werden 1 und 2 in der Schreibmethode neu angeordnet. Thread 1 weist zuerst das Flag dem True zu und führt sie dann auf Thread 2 aus, berechnet das Ergebnis direkt und dann zu Thread 1. zu diesem Zeitpunkt wird A 2, was offensichtlich einen Schritt später ist. Zu diesem Zeitpunkt können Sie das volatile Schlüsselwort zum Flagge hinzufügen, das Neubestehen verbieten, was die Ordnung des Programms sicherstellen kann, und Sie können auch Synchronisierungs- und Sperren mit Schwergewicht verwenden, um die Ordnung zu gewährleisten. Sie können sicherstellen, dass der Code in diesem Bereich gleichzeitig ausgeführt wird. Darüber hinaus hat JMM eine angeborene Reihenfolge, dh Ordnung, die ohne Mittel garantiert werden kann, was normalerweise als Prinzip als vorhanden bezeichnet wird. << JSR-133: Java-Speichermodell und Threadspezifikation >> Definiert die folgenden Vorschriften. Volatile Domäne 4. Transitivität: Wenn A vor der B und B vor der C geschieht, dann ist A vor C 5.Start () Regeln: Wenn Thread A ein Operation threadb_start () (Start Thread B) führt, dann tworb_Start () passiert vor dem Thread-A-Operation. Vor dem Thread ist ein erfolgreicher Threadb.Join () -Operation in Thread A. 7. Interrupt () Prinzip: Der Aufruf zum Thread interrupt () erfolgt zuerst, wenn das Interrupt-Ereignis durch den unterbrochenen Threadcode erkannt wird. Sie können die Thread -Methode verwenden, um festzustellen, ob eine Unterbrechung vorliegt. 8. Finalize () Prinzip: Der Abschluss eines Objekts findet zuerst statt, wenn die Methode endgültig () beginnt. Die erste Regel der Programmsequenzregel besagt, dass in einem Thread alle Vorgänge nacheinander sind, aber in JMM, solange das Ausführungsergebnis dieselbe ist, ist eine Neuordnung zulässig. Der Fokus von passiert-vor Ort ist auch die Richtigkeit des Einzel-Thread-Ausführungsergebnisses, aber es kann nicht garantiert werden, dass das gleiche für Multi-Threading gilt. Regel 2 Die Monitorregeln sind tatsächlich leicht zu verstehen. Bevor das Schloss hinzugefügt wird, können Sie das Schloss nur weiter hinzufügen. Regel 3 gilt für die volatilen fraglichen. Wenn ein Thread zuerst eine Variable schreibt und ein anderer Thread ihn liest, muss die Schreibvor Operation vor der Lesevorrichtung bestehen. Die vierte Regel ist die Transitivität von passiert. Ich werde nicht auf Details über die folgenden wenigen eingehen.
Dann müssen wir volatile variable Regeln neu erwähnt: Schreiben Sie eine flüchtige Domäne, bevor Sie diese flüchtige Domäne später lesen. Lassen Sie mich das wieder herausnehmen. Wenn eine Variable als volatil deklariert wird, kann ich beim Lesen der Variablen immer ihren neuesten Wert lesen. Hier bedeutet der neueste Wert, dass unabhängig davon, welcher Thread die Variable schreibt, sofort auf den Hauptspeicher aktualisiert wird. Ich kann auch den neu geschriebenen Wert aus dem Hauptspeicher lesen. Mit anderen Worten, das volatile Schlüsselwort kann Sichtbarkeit und Ordnung sicherstellen. Nehmen wir den obigen Code als Beispiel:
int a = 0; bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Dieser Code ist nicht nur durch Neubestehen beunruhigt, auch wenn 1 und 2 nicht neu angeordnet sind. 3 wird auch nicht so reibungslos ausgeführt. Nehmen wir an, dass Thread 1 zuerst die Schreiboperation ausführt, und Thread 2 führt dann den Multiplikationsvorgang aus. Da Thread 1 im Arbeitsspeicher 1 Flag zuweist, wird er möglicherweise nicht sofort in den Hauptspeicher zurückgeschrieben. Wenn Thread 2 ausgeführt wird, liest sich der Flag -Wert aus dem Hauptspeicher aus, was möglicherweise noch falsch ist, sodass die Anweisungen in Klammern nicht ausgeführt werden. Wenn in Folgendes geändert:
int a = 0; volatile bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Dann führt Thread 1 zuerst Schreiben aus und Thread 2 führt dann Multiply aus. Nach dem Prinzip vor dem Fall wird dieser Prozess die folgenden drei Arten von Regeln erfüllen: Programmreihenregeln: 1 passiert vore vor 2; 3 passiert bis 4; (Volatile schränkt die Anordnung der Anweisungen ein, so dass 1 vor 2 ausgeführt wird. Beim Lesen einer volatilen Variablen setzt JMM den lokalen Speicher, der dem Thread entspricht, zum ungültig erklärt, und der Thread liest die gemeinsame Variable aus dem Hauptspeicher als nächstes.
Zunächst meine Antwort lautet, dass die Atomizität nicht garantiert werden kann. Wenn es garantiert ist, ist es nur Atomizität für das Lesen/Schreiben einer einzelnen volatilen Variablen, aber es gibt nichts mit zusammengesetzten Operationen wie Volatile ++ zu tun, wie z. B. das folgende Beispiel:
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(); } while (Thread.ActiveCount ()> 1) // Stellen Sie sicher, dass die vorherigen Threads Thread abgeschlossen haben. System.out.println (test.inc); }Logischerweise beträgt das Ergebnis 10.000, aber es ist wahrscheinlich ein Wert von weniger als 10.000 beim Laufen. Einige Leute können sagen, dass volatile die Sicht nicht garantiert. Ein Thread sollte die Modifikationen an Inc von Inc sehen, und der andere Thread sollte es sofort sehen! Aber das Operation Inc ++ ist hier eine zusammengesetzte Operation, einschließlich des Lesens des Werts von INC, der Erhöhung von selbst und dann wieder an den Hauptspeicher. Angenommen, Thread A liest den Wert von INC auf 10 und wird zu diesem Zeitpunkt blockiert, da die Variable nicht geändert wird und die flüchtige Regel nicht ausgelöst werden kann. Thread B liest auch den Wert von Inc zu diesem Zeitpunkt. Der Wert von INC im Hauptspeicher beträgt immer noch 10 und wird automatisch erhöht und wird dann in den Hauptspeicher zurückgeschrieben, der 11 ist. Zu diesem Zeitpunkt ist es an der Reihe von Thread A. Da 10 im Arbeitsgedächtnis gespeichert wird, erhöht es sich weiterhin selbst und schreibt in das Hauptgedächtnis zurück. 11 ist wieder geschrieben. Obwohl die beiden Threads zweimal erhöht (), fügten sie nur einmal hinzu. Manche Leute sagen, ungläubig unglücklich die Cache -Linie? Vor dem Thread A liest Thread b und führt Vorgänge aus, der INC -Wert wird nicht geändert. Wenn Thread B liest, heißt es immer noch 10. Einige Leute sagen auch, dass, wenn Thread B 11 zurück zum Hauptspeicher schreibt, die Cache -Zeile von Thread A nicht auf ungültig erklärt wird? Thread A hat jedoch bereits den Lesevorgang durchgeführt. Nur wenn der Lesevorgang durchgeführt wird und die Cache -Zeile ungültig ist, wird der Hauptspeicherwert gelesen. Daher kann Thread A weiterhin selbst die Selbststörung durchführen. Zusammenfassend kann die Atomfunktion in dieser Art von Verbundbetrieb nicht beibehalten werden. Im obigen Beispiel des Einstellungs-Flag-Werts kann jedoch der Lese-/Schreibbetrieb von Flags einstufig sind, sondern auch die Atomizität sicherstellen. Um die Atomizität zu gewährleisten, können wir nur synchronisierte, lockige und atomare Atombetriebklassen unter gleichzeitigen Paketen verwenden, dh die Selbstbeschuldigung (add 1 Operation), Selbstverstärkung (reduzieren 1 Betrieb), Additionsbetrieb (Hinzufügen einer Zahl) und Subtraktionsbetrieb (Subtrahieren Sie eine Nummer) der Basistypen, um sicherzustellen, dass diese Operationen diese Operationen für Atomoperationen sicherstellen.
Wenn Sie Assembly -Code mit dem volatilen Schlüsselwort und dem Code ohne volatiles Schlüsselwort generieren, werden Sie feststellen, dass der Code mit dem volatilen Schlüsselwort ein zusätzliches Sperren -Präfix -Befehl hat. Das Sperrenpräfix -Befehl entspricht tatsächlich einer Speicherbarriere. Die Speicherbarriere liefert die folgenden Funktionen: 1. Beim Nachbestellen können die folgenden Anweisungen nicht vor der Speicherbarriere in den Speicherort neu angeordnet werden.
1. Die Statusmenge Marke, genau wie die Flagge oben, werde ich es erneut erwähnen:
int a = 0; volatile bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Dieser Lesen- und Schreibvorgang in Variablen, die als flüchtig markiert sind, können sicherstellen, dass Änderungen für den Thread sofort sichtbar sind. Im Vergleich zu synchronisiert ist die Lock eine gewisse Effizienzverbesserung. 2. Implementierung des Singleton -Modus, Typische Double Check Lock (DCL)
Klasse Singleton {private volatile statische Singleton Instance = null; private Singleton () {} public static Singleton getInstance () {if (instance == null) {synchronized (Singleton.class) {if (instance == null) instance = new Singleton (); }} return Instance; }}Dies ist ein fauler Singleton -Muster. Objekte werden nur bei Verwendung erstellt. Um Anweisungen für Initialisierungsvorgänge zu vermeiden, wird der Instanz umgesetzt.
Das obige ist der gesamte Inhalt dieses Artikels über die Erläuterung der volatilen Schlüsselwörter, nach denen Java -Interviewer gerne im Detail fragen. Ich hoffe, es wird für alle hilfreich sein. Interessierte Freunde können weiterhin auf andere verwandte Themen auf dieser Website verweisen. 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!