Java言語の揮発性変数は、「より軽い同期」と見なすことができます。同期されたブロックと比較して、揮発性変数はエンコードとランタイムのオーバーヘッドをより少なくする必要がありますが、達成できる機能は同期の一部にすぎません。
ロック
ロックは、相互排除と可視性の2つの主な機能を提供します。
揮発性変数
揮発性変数には、同期の可視性特性がありますが、原子特性はありません。これは、スレッドが揮発性変数の最新値を自動的に発見できることを意味します。
揮発性変数は、スレッドの安全性を提供するために使用できますが、非常に限られたユースケースのセットにのみ適用できます。複数の変数または変数の現在の値と変更された値の間に制約はありません。したがって、揮発性のみを使用するだけでは、複数の変数に関連付けられた不変性を持つカウンター、ミューテックス、または任意のクラスを実装するのに十分ではありません(例:「start <= end」)。
単純化またはスケーラビリティのために、ロックの代わりに揮発性変数を使用する傾向があります。一部のイディオムは、ロックの代わりに揮発性変数を使用するときにエンコードして読みやすくなります。さらに、揮発性変数はロックのような糸の詰まりを引き起こさないため、スケーラビリティの問題を引き起こすことはめったにありません。場合によっては、揮発性変数は、読み取り操作が書き込み操作よりもはるかに大きい場合、ロックよりもパフォーマンスの利点を提供することもできます。
揮発性変数を正しく使用する条件
限られたケースでは、ロックの代わりに揮発性変数のみを使用できます。揮発性変数の理想的なスレッドの安全性を実現するには、次の2つの条件を同時に満たす必要があります。
変数への操作の書き込みは、現在の値に依存しません。
この変数は、他の変数との不変に含まれていません。
実際、これらの条件は、揮発性変数に書き込むことができるこれらの有効な値は、変数の現在の状態を含むあらゆるプログラムの状態とは無関係であることを示しています。
最初の条件は、揮発性変数がスレッドセーフカウンターとして使用されるのを防ぎます。インクリメンタル操作(x ++)は別の操作のように見えますが、実際には、原子的に実行する必要があり、揮発性が必要な原子特性を提供することはできない、read-modify-write操作のシーケンスで構成される複合操作です。正しい操作を実装するには、操作中にxの値を一定に保つ必要がありますが、これは揮発性変数では不可能です。 (ただし、値が単一のスレッドからのみ記述されるように調整されている場合、最初の条件は無視できます。)
ほとんどのプログラミング状況は、これら2つの条件のいずれかと矛盾しているため、揮発性変数は、同期して安全性に合わせて普遍的に適用できません。リスト1は、非スレッドセーフ数値範囲クラスを示しています。それには不変が含まれています - 下限は常に上限以下です。
例を挙げてください
以下の例を見てみましょう。カウンターを実装します。スレッドが始まるたびに、カウンターINCメソッドが呼び出され、実行環境にカウンターを追加します-JDKバージョン:JDK1.6.0_31、メモリ:3G CPU:X86 2.4G
パブリッククラスカウンター{public static int count = 0; public static void inc(){//ここでの遅延は1ミリ秒です。 } catch(arternedexception e){} count ++; } public static void main(string [] args){//同時に1000スレッドを開始してi ++計算を実行し、(int i = 0; i <1000; i ++){new runnable(){@Override public void run(){counter.inc();}) } //ここでの各実行の値は異なる場合があり、おそらく1000 system.out.println( "run result:counter.count =" + counter.count); }}実行結果:Counter.Count = 995
実際の操作結果は毎回異なる場合があります。マシンの結果は次のとおりです。実行結果:counter.count = 995。マルチスレッド環境では、Countが結果が1000になるとは予想していないことがわかります。
多くの人々は、これがマルチスレッドの並行性の問題であると考えています。この問題を回避するために、変数カウントの前に揮発性を追加するだけです。次に、コードを変更して、結果が期待を満たしているかどうかを確認します。
public class counter {public volatile static int count = 0; public static void inc(){//ここでの遅延は1ミリ秒です。 } catch(arternedexception e){} count ++; } public static void main(string [] args){//同時に1000スレッドを開始し、i ++計算を実行し、(int i = 0; i <1000; i ++){new runnable(){@override public void run(){counter.inc();}) } //ここでの各実行の値は異なる場合があります。 }}実行結果:Counter.Count = 992
操作の結果は、予想されるほど1000ではありません。以下の理由を分析しましょう
Java Garbage Collectionの記事では、JVMの瞬間にメモリの割り当てが説明されています。メモリ領域の1つは、JVM仮想マシンスタックです。各スレッドには、実行時にスレッドスタックがあり、スレッドスタックはスレッド実行中に変数値情報を保存します。スレッドが特定のオブジェクトの値にアクセスすると、最初にオブジェクトの参照を使用してヒープメモリに対応する変数の値を見つけ、次にヒープメモリ変数の特定の値をスレッドのローカルメモリにロードして変数コピーを作成します。その後、スレッドはオブジェクトのヒープメモリ変数値と関係がありませんが、コピー変数の値を直接変更し、変更後の特定の瞬間(スレッドが終了する前)に、スレッド変数コピーの値をオブジェクトのヒープ変数に自動的に書き戻します。このようにして、ヒープ内のオブジェクトの値が変わります。次の写真は、この執筆の相互作用について説明しています
メインメモリから現在のワーキングメモリまでのEADおよびロードコピー変数
実行コードを使用して割り当てて共有変数値を変更します
ワーキングメモリデータでメインメモリ関連のコンテンツをリフレッシュして書き留める
使用と割り当ては複数回表示できます
ただし、これらの操作は原子ではありません。つまり、読み取り負荷後、メインメモリカウント変数が変更された場合、スレッドワーキングメモリの値はロードされているため、対応する変更を引き起こさないため、計算された結果は予想とは異なります。
揮発性によって変更された変数の場合、JVM仮想マシンは、メインメモリからスレッドワーキングメモリにロードされた値が最新であることのみを保証します
たとえば、スレッド1とスレッド2が読み取り操作とロード操作を実行し、メインメモリのカウント値が5であることがわかった場合、最新の値はロードされます
ヒープカウントがスレッド1で変更されると、メインメモリに書き込まれ、メインメモリのカウント変数は6になります。
Thread 2はすでに読み取りおよび負荷操作を実行しているため、メインメモリカウントの変動値も操作後6に更新されます。
これにより、2つのスレッドが時間内に揮発性キーワードで変更された後、同時性が発生します。