この Java volatile キーワードは、Java 変数を「メイン メモリに格納されている」とマークするために使用されます。より正確には、揮発性変数の読み取りはすべて CPU キャッシュからではなくコンピューターのメイン メモリから読み取られ、揮発性変数へのすべての書き込みは CPU キャッシュに書き込むだけではなくメイン メモリに書き込まれることを意味します。
実際、Java5 以降、volatile キーワードにより、変数がメイン メモリに書き込まれるだけでなく、メイン メモリから読み取られることも保証されます。次のセクションで説明します。
Java の volatile キーワードの可視性の保証
この Java の volatile キーワードは、スレッド間での変数の変更の可視性を保証します。これは少し抽象的に聞こえるかもしれないので、詳しく説明しましょう。
マルチスレッド アプリケーションでは、パフォーマンス上の理由から、各スレッドは動作時にメイン メモリから CPU キャッシュに変数をコピーすることがあります。コンピュータに複数の CPU が搭載されている場合、各スレッドは異なる CPU で実行される可能性があります。つまり、各スレッドは変数を異なる CPU の CPU キャッシュにコピーします。以下の図に示すように:
不揮発性変数を使用すると、JVM がいつメイン メモリから CPU キャッシュにデータを読み取ったり、CPU キャッシュからメイン メモリにデータを書き込んだりするかという保証はありません。これにより、次のセクションで説明するいくつかの問題が発生する可能性があります。
次のように、宣言されたカウンタ変数を含む共有オブジェクトに 2 つ以上のスレッドがアクセスするシナリオを想像してください:
public class SharedObject { public int counter = 0; }
また、スレッド 1 だけがカウンタ変数をインクリメントするが、スレッド 1 とスレッド 2 も増加すると想像してください。時々、このカウンタ変数を読み取ってください。
カウンタ変数が volatile として宣言されていない場合、いつカウンタ変数が CPU キャッシュからメインメモリに書き戻されるかという保証はありません。これは、CPU キャッシュ内のカウンタ変数がメイン メモリ内の値と異なることを意味します。以下の図に示すように:
スレッドがこの変数の最新の値を確認できないという問題は、この変数が他のスレッドによってメイン メモリに書き戻されていないためです。これを「可視性」とは呼びません。 " 問題。あるスレッドからの更新は他のスレッドからは見えません。
カウンタ変数を volatile として宣言すると、カウンタ変数へのすべての書き込みが直ちにメイン メモリに書き戻されます。同時に、カウンタ変数の読み取りはすべてメイン メモリから直接読み取られます。カウンタ変数を volatile として宣言する方法は次のとおりです。
public class SharedObject { public volatile int counter = 0; }
変数を volatile として宣言すると、この変数への書き込みが他のスレッドから見えるようになります。
この Java volatile キーワードは、優先順位と順序を保証します
Java5 以降、この volatile キーワードは、メイン メモリからの変数の読み取りと書き込みを保証するだけではありません。実際、 volatile キーワードはこれも保証します:
スレッド A が volatile 変数に書き込み、その後スレッド B がこの変数を読み取る場合、この volatile 変数を書き込む前に、すべての変数がスレッド A 用に表示されます。この揮発性変数を読み取った後は、スレッド B にも表示されます。
揮発性変数の読み取りおよび書き込みの命令は、JVM によって再順序付けされません (JVM は、再順序付けによるプログラムアクティビティが変更されていないことを JVM が検出する限り、パフォーマンス上の理由から命令を再順序付けすることがあります)。命令は前後に並べ替えることができますが、揮発性キーワードの読み取りまたは書き込みはこれらの命令と混合されません。揮発性変数の読み取りまたは書き込みの後にどの命令が続くかに関係なく、読み取りまたは書き込みの順序は保証されます。
これらの表現にはさらに深い説明が必要です。
スレッドが volatile 変数を書き込むとき、volatile 変数自体がメイン メモリに書き戻されるだけではありません。この揮発性変数を書き込む前にこのスレッドによって変更された他のすべての変数もメイン メモリに書き戻されます。スレッドが volatile 変数を読み取るとき、volatile 変数とともにメイン メモリに書き戻される他のすべての変数も読み取ります。
この例を見てください:
Thread A: sharedObject.nonVolatile = 123; sharedObject.counter = sharedObject.counter + 1; Thread B: int counter = sharedObject.counter; int nonVolatile = sharedObject.nonVolatile;
なぜなら、この揮発性カウンタを書き込む前に、スレッド A は不揮発性の nonVolatile 変数を書き込み、スレッド A がこのカウンタ (揮発性変数) を書き込むときに、不揮発性変数もメインメモリに書き戻されます。
スレッドBが揮発性変数カウンターの読み取りを開始するため、カウンター変数と非揮発性変数はスレッドBによってメインメモリからCPUキャッシュに読み取られます。このとき、スレッド B には、スレッド A によって書き込まれた nonVolatile 変数も表示されます。
開発者は、この拡張可視性保証を使用して、スレッド間の変数の可視性を最適化できます。すべての変数を volatile として宣言するのではなく、1 つまたはいくつかの変数のみを volatile として宣言する必要があります。以下に例を示します:
public class Exchanger { private Object object = null; private volatile hasNewObject = false; public void put(Object newObject) { while(hasNewObject) { //wait - do not overwrite existing new object } object = newObject; hasNewObject = true; //volatile write } public Object take(){ while(!hasNewObject){ //volatile read //wait - don't take old object (or null) } Object obj = object; hasNewObject = false; //volatile write return obj; } }
线程A可能会通过不断的调用put方法设置对象。线程B可能会通过不断的调用take方法获取这个对象。这个类可以工作的很好通过使用一个volatile变量(没有使用synchronized锁),只要只是线程A调用put方法,线程B调用take方法。
然而,JVM可能重排序Java指令去优化性能,如果JVM可以做这个没有改变这个重排序的指令。如果JVM改变了put方法和take方法内部的读和写的顺序将会发生什么呢?如果put方法真的像下面这样执行:
while(hasNewObject) { //wait - do not overwrite existing new object } hasNewObject = true; //volatile write object = newObject;
注意这个volatile变量的写是在新的对象被真实赋值之前执行的。对于JVM这个可能看起来是完全正确的。这两个写的执行的值不会互相依赖。
然而,重排序这个执行的执行将会危害object变量的可见性。首先,线程B可能在线程A确定的写一个新的值给object变量之前看到hasNewObject这个值设为true了。第二,现在甚至不能保证对于object的新的值是否会写回到主内存中。
为了阻止上面所说的那种场景发生,这个volatile关键字提供了一个“发生前保证”。保证volatile变量的读和写指令执行前不会发生重排序。指令前和后是可以重排序的,但是这个volatile关键字的读和写指令是不能发生重排序的。
看这个例子:
sharedObject.nonVolatile1 = 123; sharedObject.nonVolatile2 = 456; sharedObject.nonVolatile3 = 789; sharedObject.volatile = true; //a volatile variable int someValue1 = sharedObject.nonVolatile4; int someValue2 = sharedObject.nonVolatile5; int someValue3 = sharedObject.nonVolatile6;
JVM可能会重排序前面的三个指令,只要他们中的所有在volatile写执行发生前(他们必须在volatile写指令发生前执行)。
类似的,JVM可能重排序最后3个指令,只要volatile写指令在他们之前发生。最后三个指令在volatile写指令之前都不会被重排序。
那个基本上就是Java的volatile保证先行发生的含义了。
volatile关键字不总是足够的
甚至如果volatile关键字保证了volatile变量的所有读取都是从主内存中读取,以及所有的写也是直接写入到主内存中,但是这里仍然有些场景声明volatile是不够的。
在更早解释的场景中,只有线程1写这个共享的counter变量,声明这个counter变量为volatile是足够确保线程2总是看到最新写的值。
事实上,如果在写这个变量的新的值不依赖它之前的值得情况下,甚至多个线程写这个共享的volatile变量,仍然有正确的值存储在主内存中。换句话说,如果一个线程写一个值到这个共享的volatile变量值中首先不需要读取它的值去计算它的下一个值。
如果一个线程需要首先去读取这个volatile变量的值,并且建立在这个值的基础上去生成一个新的值,那么这个volatile变量对于保证正确的可见性就不够了。在读这个volatile变量和写新的值之间的短时间间隔,出现了一个竞态条件,在这里多个线程可能会读取到volatile变量的相同的值生成一个新的值,并且当写回到主内存中的时候,会互相覆盖彼此的值。
多个线程增加相同的值得这个场景,正好一个volatile变量不够的。下面的部分将会详细解析这个场景。
想象下,如果线程1读取值为0的共享变量counter进入到CPU缓存中,增加1并且没有把改变的值写回到主内存中。线程2读取相同的counter变量从主内存中进入到CPU缓存中,这个值仍然为0。线程2也是加1,并且也没有写入到主内存中。这个场景如下图所示:
线程1和线程2现在是不同步的。这个共享变量的真实值应该是2,但是每一个线程在他们的CPU缓存中都为1,并且在主内存中的值仍然是0.它是混乱的。甚至如果线程最后写他们的值进入主内存中,这个值是错误的。
什么时候volatile是足够的
正如我前面提到的,如果两个线程都在读和写一个共享的变量,然后使用volatile关键字是不够的。你需要使用一个synchronized在这种场景去保证这个变量的读和写是原子性的。读或者写一个volatile变量不会堵塞正在读或者写的线程。因为这个发生,你必须使用synchronized关键字在临界区域周围。
作为一个synchronized锁可选择的,你也可以使用在java.util.concurrent包中的许多原子数据类型中的一个。例如,这个AtomicLong或者AtomicReference或者是其他中的一个。
假如只有一个线程读和写这个volatile变量的值,其他的线程只是读取这个变量,然后读的这个线程就会保证看到最新的值了。不使用这个volatile变量,这个就不能保证。
volatile キーワードのパフォーマンスに関する考慮事項
volatile 変数の読み取りおよび書き込みにより、この変数はメイン メモリに読み取られるか、メイン メモリに書き込まれます。メイン メモリの読み取りまたは書き込みは、CPU キャッシュにアクセスするよりもコストがかかります。揮発性変数にアクセスすると、標準的なパフォーマンス向上手法である命令の並べ替えも防止されます。したがって、変数の強力な可視性が本当に必要な場合にのみ、揮発性変数を使用する必要があります。
上記は Java の Volatile キーワードの詳細な説明です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) を参照してください。