1. volatile の役割
「Java 同時プログラミング: 基本理論」の記事で、可視性、順序付け、原子性の問題についてはすでに述べましたが、通常、これらは Synchronized キーワードによって解決できます。 Synchronized の原理を理解していれば、Synchronized は比較的重量のある操作であり、システムのパフォーマンスに比較的大きな影響を与えることがわかるはずです。そのため、他の解決策がある場合は、通常、問題を解決するために Synchronized を使用することは避けられます。 volatile キーワードは、可視性と順序の問題を解決するために Java で提供されるもう 1 つのソリューションです。アトミック性については、強調しておく必要があり、誤解されやすい点でもあります。volatile 変数に対する 1 回の読み取り/書き込み操作では、long 型変数や double 型変数などのアトミック性は保証されますが、アトミック性は保証されません。 i++ は基本的に読み取りと書き込みの 2 回の操作であるためです。
2. volatile の使用
volatile の使用については、いくつかの例を使用してその使用法とシナリオを説明します。
1. 並べ替えを防ぐ
最も古典的な例から並べ替えの問題を分析します。誰もがシングルトン パターンの実装に精通している必要があります。並行環境でシングルトンを実装するには、通常、ダブル チェック ロック (DCL) を使用します。ソース コードは次のとおりです。
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 }
次に、なぜ volatile キーワードがシングルトン変数の間に追加されるのかを分析します。この問題を理解するには、まずオブジェクトのインスタンス化プロセスを理解する必要があります:
(1) メモリ領域を割り当てる。
(2) オブジェクトを初期化します。
(3) メモリ空間のアドレスを対応する参照に代入します。
しかし、オペレーティングシステムは命令を並べ替えることができるため、上記のプロセスは次のプロセスになる場合もあります:
(1) メモリ空間を割り当てます。
(2) メモリ空間のアドレスを対応する参照に代入します。
(3) オブジェクトの初期化
このプロセスの場合、初期化されていないオブジェクト参照がマルチスレッド環境に公開され、予期しない結果が生じる可能性があります。したがって、このプロセスの順序変更を防ぐために、変数を volatile 型変数に設定する必要があります。
2. 可視性を実装する
可視性の問題は主に、あるスレッドが共有変数の値を変更するが、別のスレッドがそれを認識できないことを意味します。可視性の問題の主な理由は、各スレッドが独自のキャッシュ領域とスレッドの作業メモリを持っていることです。 volatile キーワードはこの問題を効果的に解決できます。その効果を知るために次の例を見てみましょう。
直观上说,这段代码的结果只可能有两种: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="volatile の使用方法とその原理の分析の詳細な説明"></span></div>
変数 i にキーワード volatile を追加した後、このプログラムはスレッドセーフであると誤解するかもしれません。上記のプログラムを実行してみてください。以下は私の現地での手術結果です:
皆さんの手術結果は異なる可能性があります。ただし、volatile はアトミック性を保証できないことに注意してください (そうでない場合、結果は 1000 になるはずです)。理由も非常に簡単です。i++ は実際には次の 3 つのステップを含む複合演算です:
(1) i の値を読み取ります。
(2) iに1を加えます。
(3) i の値をメモリに書き戻します。
Volatile は、これら 3 つの操作がアトミックであることを保証できません。AtomicInteger または Synchronized を使用して、+1 操作のアトミック性を保証できます。
注: Thread.sleep() メソッドは、上記のコードの多くの場所で実行されます。その目的は、同時実行の問題が発生する可能性を高めることであり、それ以外の効果はありません。
3. volatile の原理
上記の例を通じて、基本的に volatile とは何か、そしてその使用方法を理解する必要があります。次に、volatile の最下層がどのように実装されるかを見てみましょう。
1. 可視性の実装:
前の記事で述べたように、スレッド自体はデータのメインメモリと直接対話しませんが、スレッドの作業メモリを通じて対応する操作を完了します。これは、スレッド間のデータが非表示になる本質的な理由でもあります。したがって、揮発性変数の可視性を実現するには、この側面から始めてください。 volatile 変数への書き込み操作と通常の変数への書き込み操作には主に 2 つの違いがあります:
(1) volatile 変数が変更されると、変更された値はメイン メモリ内で強制的にリフレッシュされます。
(2) volatile 変数を変更すると、他のスレッドの作業メモリ内の対応する変数値が無効になります。したがって、変数の値を読み出す場合には、メインメモリ上の値を再度読み込む必要があります。
これら 2 つの操作を通じて、volatile 変数の可視性の問題を解決できます。
2. 順序付けされた実装:
この問題を説明する前に、まず Java の Happen-before ルールが JSR 133 で次のように定義されていることを理解しましょう:
2 つのアクションは、happen-before によって順序付けできます。 relationship.If one action happens before another, then the first is believe to and order before the Second.
平たく言えば、a happens-before b の場合、a によって行われた操作はすべて of b に表示されます。 (「前に起こる」という言葉は時間の前後を意味すると誤解されやすいため、誰もがこれを覚えておく必要があります)。 JSR 133 で定義されている前発生ルールを見てみましょう。
• スレッド内の各アクションは、そのスレッド内のその後のすべてのアクションの前に発生します
• モニター上のロック解除は、そのモニター上のすべての後続のロックより前に発生します。 • volatile フィールドへの書き込みは、その後のその volatile の読み取りの前に行われます
• スレッド上の start() の呼び出しは、開始されたスレッド内のアクションの前に行われます
• スレッド内のすべてのアクションは、他のスレッドが正常に戻る前に行われます。そのスレッドの join() から
• アクション a がアクション b の前に発生し、b がアクション c の前に発生する場合、a は c の前に発生します
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)该变量没有包含在具有其他变量的不变式中。
以上がvolatile の使用方法とその原理の分析の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。