1. Die Rolle von Volatilität
Im Artikel „Java Concurrent Programming: Core Theory“ haben wir bereits die Themen Sichtbarkeit, Ordnung und Atomizität erwähnt Lösen Sie diese Probleme mit dem Schlüsselwort Synchronized. Wenn Sie jedoch das Prinzip von Synchronized verstehen, sollten Sie wissen, dass Synchronized ein relativ schwerer Vorgang ist und einen relativ großen Einfluss auf die Leistung des Systems hat Vermeiden Sie normalerweise die Verwendung von Synchronized zur Lösung von Problemen. Das Schlüsselwort volatile ist eine weitere in Java bereitgestellte Lösung, um das Problem der Sichtbarkeit und Reihenfolge zu lösen. In Bezug auf die Atomizität muss eines betont werden, und es ist auch ein Punkt, der leicht missverstanden werden kann: Ein einzelner Lese-/Schreibvorgang für flüchtige Variablen kann Atomizität garantieren, z. B. Variablen vom Typ Long und Double, garantiert jedoch nicht die Atomizität von Operationen wie i++, da i++ im Wesentlichen eine zweifache Operation aus Lesen und Schreiben ist.
2. Die Verwendung von volatile
Bezüglich der Verwendung von volatile können wir mehrere Beispiele verwenden, um seine Verwendung und Szenarien zu veranschaulichen.
1. Nachbestellung verhindern
Wir analysieren das Nachbestellungsproblem anhand des klassischsten Beispiels. Jeder sollte mit der Implementierung des Singleton-Musters vertraut sein. Um einen Singleton in einer gleichzeitigen Umgebung zu implementieren, können wir normalerweise Double Check Locking (DCL) verwenden. Der Quellcode lautet wie folgt:
1 package com.paddx.test.concurrent; 2 3 public class Singleton { 4 public static volatile Singleton singleton; 5 6 /** 7 * 构造函数私有,禁止外部实例化 8 */ 9 private Singleton() {}; 10 11 public static Singleton getInstance() { 12 if (singleton == null) { 13 synchronized (singleton) { 14 if (singleton == null) { 15 singleton = new Singleton(); 16 } 17 } 18 } 19 return singleton; 20 } 21 }
Lassen Sie uns nun analysieren, warum wir das Schlüsselwort volatile zwischen den Variablen Singleton hinzufügen müssen. Um dieses Problem zu verstehen, müssen Sie zunächst verstehen, dass der Objektkonstruktionsprozess tatsächlich in drei Schritte unterteilt werden kann:
(1) Speicherplatz zuweisen.
(2) Initialisieren Sie das Objekt.
(3) Weisen Sie die Adresse des Speicherplatzes der entsprechenden Referenz zu.
Aber da das Betriebssystem Anweisungen neu anordnen kann, kann der obige Prozess auch zu dem folgenden Prozess werden:
(1) Speicherplatz zuweisen.
(2) Ordnen Sie die Adresse des Speicherplatzes der entsprechenden Referenz zu.
(3) Initialisieren des Objekts
Wenn dies der Fall ist, kann eine nicht initialisierte Objektreferenz in einer Multithread-Umgebung verfügbar gemacht werden, was zu unvorhersehbaren Ergebnissen führt. Um eine Neuordnung dieses Prozesses zu verhindern, müssen wir die Variable daher auf eine Variable vom Typ „flüchtig“ setzen.
2. Sichtbarkeit implementieren
Das Sichtbarkeitsproblem bedeutet hauptsächlich, dass ein Thread den Wert der gemeinsam genutzten Variablen ändert, ein anderer Thread ihn jedoch nicht sehen kann. Der Hauptgrund für das Sichtbarkeitsproblem besteht darin, dass jeder Thread über einen eigenen Cache-Bereich und Thread-Arbeitsspeicher verfügt. Das Schlüsselwort volatile kann dieses Problem effektiv lösen. Schauen wir uns das folgende Beispiel an, um seine Wirkung zu sehen:
1 package com.paddx.test.concurrent; 2 3 public class VolatileTest { 4 int a = 1; 5 int b = 2; 6 7 public void change(){ 8 a = 3; 9 b = a; 10 } 11 12 public void print(){ 13 System.out.println("b="+b+";a="+a); 14 } 15 16 public static void main(String[] args) { 17 while (true){ 18 final VolatileTest test = new VolatileTest(); 19 new Thread(new Runnable() { 20 @Override 21 public void run() { 22 try { 23 Thread.sleep(10); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 test.change(); 28 } 29 }).start(); 30 31 new Thread(new Runnable() { 32 @Override 33 public void run() { 34 try { 35 Thread.sleep(10); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 test.print(); 40 } 41 }).start(); 42 43 } 44 } 45 }
直观上说,这段代码的结果只可能有两种:b=3;a=3 或 b=2;a=1。不过运行上面的代码(可能时间上要长一点),你会发现除了上两种结果之外,还出现了第三种结果:
......
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......
为什么会出现b=3;a=1这种结果呢?正常情况下,如果先执行change方法,再执行print方法,输出结果应该为b=3;a=3。相反,如果先执行的print方法,再执行change方法,结果应该是 b=2;a=1。那b=3;a=1的结果是怎么出来的?原因就是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才出现这一结果。如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。
3、保证原子性
关于原子性的问题,上面已经解释过。volatile只能保证对单次读/写的原子性。这个问题可以看下JLS中的描述:
For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.
Writes and reads of volatile long and double values are always atomic.
Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.
Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.
这段话的内容跟我前面的描述内容大致类似。因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
关于volatile变量对原子性保证,有一个问题容易被误解。现在我们就通过下列程序来演示一下这个问题:
1 package com.paddx.test.concurrent; 2 3 public class VolatileTest01 { 4 volatile int i; 5 6 public void addI(){ 7 i++; 8 } 9 10 public static void main(String[] args) throws InterruptedException { 11 final VolatileTest01 test01 = new VolatileTest01(); 12 for (int n = 0; n <div class="cnblogs_code_toolbar"><span class="cnblogs_code_copy"><img src="https://img.php.cn/upload/article/000/000/001/0313942818f37f12b8713dd37831b872-5.gif" alt="Detaillierte Erläuterung der Verwendung von Volatilität und Analyse seiner Prinzipien"></span></div>
Sie denken möglicherweise fälschlicherweise, dass dieses Programm nach dem Hinzufügen des Schlüsselworts volatile zur Variablen i threadsicher ist. Sie können versuchen, das obige Programm auszuführen. Folgendes ist das Ergebnis meiner lokalen Operation:
Das Ergebnis kann bei jedem unterschiedlich sein. Es sollte jedoch beachtet werden, dass volatile keine Atomizität garantieren kann (andernfalls sollte das Ergebnis 1000 sein). Der Grund ist auch sehr einfach. i++ ist eigentlich eine zusammengesetzte Operation, die drei Schritte umfasst:
(1) Lesen Sie den Wert von i.
(2) Addiere 1 zu i.
(3) Schreiben Sie den Wert von i zurück in den Speicher.
Volatile kann nicht garantieren, dass diese drei Operationen atomar sind. Wir können AtomicInteger oder Synchronized verwenden, um die Atomizität der +1-Operation sicherzustellen.
Hinweis: Die Thread.sleep()-Methode wird an vielen Stellen im obigen Code ausgeführt. Der Zweck besteht darin, die Wahrscheinlichkeit von Parallelitätsproblemen zu erhöhen, und hat keine anderen Auswirkungen.
3. Das Prinzip von Volatilität
Anhand der obigen Beispiele sollten wir grundsätzlich wissen, was Volatilität ist und wie man es verwendet. Schauen wir uns nun an, wie die unterste Schicht von Volatile implementiert wird.
1. Sichtbarkeitsimplementierung:
Wie im vorherigen Artikel erwähnt, interagiert der Thread selbst nicht direkt mit dem Hauptspeicher, sondern über den Thread Schließen Sie den entsprechenden Vorgang ab. Dies ist auch der wesentliche Grund, warum Daten zwischen Threads unsichtbar sind. Um die Sichtbarkeit flüchtiger Variablen zu erreichen, beginnen Sie daher einfach mit diesem Aspekt. Es gibt zwei Hauptunterschiede zwischen Schreibvorgängen in flüchtige Variablen und gewöhnlichen Variablen:
(1) Wenn eine flüchtige Variable geändert wird, muss der geänderte Wert im Hauptspeicher aktualisiert werden.
(2) Das Ändern flüchtiger Variablen führt dazu, dass die entsprechenden Variablenwerte im Arbeitsspeicher anderer Threads ungültig werden. Wenn Sie daher den Variablenwert lesen, müssen Sie den Wert im Hauptspeicher erneut lesen.
Durch diese beiden Operationen kann das Sichtbarkeitsproblem flüchtiger Variablen gelöst werden.
2. Ordentliche Implementierung:
Bevor wir dieses Problem erklären, wollen wir zunächst die Happen-Before-Regeln in Java verstehen. Die Definition von before ist wie folgt:
Zwei Aktionen können durch eine Vorher-Beziehung angeordnet werden. Wenn eine Aktion vor einer anderen stattfindet, ist die erste für die zweite sichtbar und wird vor ihr angeordnet beliebter Das heißt, wenn a vor b passiert, ist jede von a ausgeführte Operation für b sichtbar. (Jeder muss sich daran erinnern, denn das Wort „geschehen vor“ wird leicht missverstanden und bedeutet „vor und nach der Zeit“.) Schauen wir uns an, welche Vorher-Vorher-Regeln in JSR 133 definiert sind:
• Jede Aktion in einem Thread erfolgt vor jeder nachfolgenden Aktion in diesem Thread.
• Eine Entsperrung auf einem Monitor erfolgt vor jeder weiteren Sperre auf diesem Monitor.• Ein Schreibvorgang in ein flüchtiges Feld erfolgt vor jedem weiteren Lesevorgang dieses flüchtigen Felds.
• Ein Aufruf von start() für einen Thread erfolgt vor allen Aktionen im gestarteten Thread. Alle Aktionen in einem Thread werden ausgeführt, bevor ein anderer Thread erfolgreich von einem join() in diesem Thread zurückkehrt.
• Wenn eine Aktion a vor einer Aktion b und b vor einer Aktion c ausgeführt wird, erfolgt a vor c.
Übersetzt als:
Can Reorder | 2nd operation | |||
1st operation | Normal Load Normal Store |
Volatile Load | Volatile Store | |
Normal Load Normal Store |
No | |||
Volatile Load | No | No | No | |
Volatile store | No | No |
3、内存屏障
为了实现volatile可见性和happen-befor的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。下面是完成上述规则所要求的内存屏障:
Required barriers | 2nd operation | |||
1st operation | Normal Load | Normal Store | Volatile Load | Volatile Store |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile Load | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile Store | StoreLoad | StoreStore |
(1)LoadLoad 屏障
执行顺序:Load1—>Loadload—>Load2
确保Load2及后续Load指令加载数据之前能访问到Load1加载的数据。
(2)StoreStore 屏障
执行顺序:Store1—>StoreStore—>Store2
确保Store2以及后续Store指令执行前,Store1操作的数据对其它处理器可见。
(3)LoadStore 屏障
执行顺序: Load1—>LoadStore—>Store2
确保Store2和后续Store指令执行前,可以访问到Load1加载的数据。
(4)StoreLoad 屏障
执行顺序: Store1—> StoreLoad—>Load2
确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。
最后我可以通过一个实例来说明一下JVM中是如何插入内存屏障的:
1 package com.paddx.test.concurrent; 2 3 public class MemoryBarrier { 4 int a, b; 5 volatile int v, u; 6 7 void f() { 8 int i, j; 9 10 i = a; 11 j = b; 12 i = v; 13 //LoadLoad 14 j = u; 15 //LoadStore 16 a = i; 17 b = j; 18 //StoreStore 19 v = i; 20 //StoreStore 21 u = j; 22 //StoreLoad 23 i = u; 24 //LoadLoad 25 //LoadStore 26 j = b; 27 a = i; 28 } 29 }
四、总结
总体上来说volatile的理解还是比较困难的,如果不是特别理解,也不用急,完全理解需要一个过程,在后续的文章中也还会多次看到volatile的使用场景。这里暂且对volatile的基础知识和原来有一个基本的了解。总体来说,volatile是并发编程中的一种优化,在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
(1)对变量的写操作不依赖于当前值。
(2)该变量没有包含在具有其他变量的不变式中。
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Verwendung von Volatilität und Analyse seiner Prinzipien. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!