Java並行性におけるvolatileキーワードの詳細な説明

volatileキーワードとは

volatileキーワードは、変数を変更するために使用されます。このキーワードによって変更された変数は、可視性と順序を保証できます。
しかし、それは原子性を達成することはできません。
これは、弱体化された軽量のSynchronizedキーワードと考えることができます。
同期に使用されます。

上記の3つの特性の説明から始めましょう。

3つの特徴

可視性、原子性、および順序は、Javaの同時実行性全体の基礎です。

  • 可視性:つまり、スレッドが共有変数の値を変更すると、この操作の後、他のスレッドが変数を読み取り、古いデータの代わりに変更された新しいデータを読み取ります。
  • Atomicity:操作は分割できず、中断することはできません。実行されるか実行されないかのどちらかです。実行の途中で停止するとは言えません。
  • 秩序:たとえば、コードは相対的な順序で実行されます。前の行のコードが最初に実行され、次の行のコードが後で実行されます。なんでそんなこと言うの?シングルスレッド環境では、実際にはシーケンシャルと見なすことができますが、マルチスレッドの観点からは必ずしもそうとは限りません。コンパイラとCPUは、実行効率のために正しいシングルスレッドの結果を保証することを前提として、コードまたは命令を並べ替えます。シングルスレッドの場合は影響しませんが、マルチスレッドの場合はこれが問題になります。

これらの3つの特性は、Javaの並行性で解決したいと考えている問題であると言えます。
これに対応して、JMM(Javaメモリモデル)はこれら3つの特性に基づいて構築されています。

次に、JMMを紹介します

Javaメモリモデル

ハードウェアの下部で、CPUはメインメモリ(メモリ)と相互作用する必要があります。
しかし、CPUのレジスタは高速ですが、メインメモリの速度はレジスタに比べて遅すぎます。
CPUがメモリと直接相互作用する場合、時間の無駄になります。レジスタ容量が小さく、コストが高すぎます。
したがって、最下層はキャッシュを使用してレジスタ(cpu)をメインメモリに接続します。速度は2つの間にあるので、価格も許容範囲です。両方のキャッシュとして。
現在、基本的な最下層はこのモデルを採用しています。ただし、CPUが異なればモデルも異なります。それらをプログラマーに直接マッピングすると、面倒になりすぎます。プログラマーが検討するにはシナリオが多すぎます。
そのため、Javaは、これらのモデルをカプセル化するために独自のメモリモデルを構築し、プログラマーに提供するためにデフォルトの不変のロジックをカスタマイズします。これはJavaメモリモデル(JMM)です。

つまり、根本的なメモリの状況を理解しています。Javaメモリモデルを考慮するだけでよく、CPUや混乱の種類を考慮する必要はありません。
ここに画像の説明を挿入します
JMMの概略図を上に示します。
ここでのモデルは論理的な概念であり、必ずしも現実のものではありません。プログラマーとして、それが実際に存在するかどうかを考慮する必要はありません。

各スレッドにはプライベートワーカースレッドがあり、ワーカースレッドはメインメモリに接続されています。
メインメモリはすべてのスレッドで共有され、保存されるのは共有変数(ほとんどすべてのインスタンス変数、静的変数、クラスオブジェクトなど)です。

  • スレッド読み取り操作:最初にメインメモリ内の共有変数を独自のローカルメモリにフラッシュし、次にローカルメモリから読み取ります
  • スレッド書き込み操作:最初にデータをローカルメモリに書き込み、次にデータをメインメモリにフラッシュします

ここで、読み取りか書き込みかに関係なく、アトミックではなく、2つの別々の操作の同時操作であることに注意してください。
スレッドのすべてのデータの読み取りと書き込みはローカルメモリで実行する必要があり、メインメモリを直接操作することはできません。

JMMの問題

このメモリモデルはキャッシュを実現するため、CPUのスループットが可能な限り大きくなり、低速のメモリを待つ必要がありません。これには長所と短所があります。
読み取り操作と書き込み操作を分離すると、スレッドセーフの問題が発生します。
たとえば、次の例:

	static int i =0;
	两个线程同时执行以下代码,结果会怎样呢?
	线程A执行将i=2;
	线程B读取i值;

スレッドAが最初に実行され、スレッドBが後で実行されます。iはスレッドB 2によって読み取られますか?
必ずしも0である必要はありません。
スレッドAが実行され、i = 2変数がローカルメモリに配置されると、スレッドBはメインメモリのiを自身のローカルメモリにフラッシュします。このとき、メインメモリのiは次のようになります。 0の場合、スレッドAのローカルメモリはiをメインメモリにフラッシュします。
したがって、最終的な結果は、メインメモリでiが2であり、スレッドBによって読み取られた結果が0です。ロジックによれば、Aが最初に実行され、Bが後で実行されますが、結果は2になります。しかし、そうではないので、これはJMMによって引き起こされる問題です。(ここでは、スレッドABのキャッシュ内のiの値が異なるため、キャッシュの一貫性の問題が設計されています)

命令の並べ替え

上記の問題に加えて、命令の並べ替えの問題にも直面します(コード。バイトコード命令としても理解できますが、ほとんど同じです)。

まず第一に、なぜ並べ替えるのですか?
jvmとコンパイル時の場合、現在のコードの順序は必ずしも効率的ではないため、効率を追求するには、順序を中断する必要があります。
特に並行環境では、並べ替えがより重要になります。

そのような例を見てください。
最近のCPUは通常、パイプライン技術を使用しています。
複数の命令を実行する必要があるため、各命令を異なるステップに分解することもできます。各ステップで使用されるレジスタ(リソースとしてではない)は異なります。リソースを除いて、命令の一部のみが同時に実行される場合それが占める、他のリソースが無駄になります。
そのため、命令1のパートaを最初に実行し、命令2のパートbを同時に実行し、命令3のパートdを同時に実行するなどのパイプライン技術を使用します。同時に、複数の命令が同時に実行されるため、はるかに効率的です。
同時に、命令の場合、2つのステップの順序を逆にすることができれば、ステップ2の順序でステップ3を待つ必要はありません(これはブロックされます)、実行することを選択できますステップ2または最良の方法に従って最初にステップします。3。このように、命令を並べ替えると、より効率的になります。

この例と同じように、効率を上げるために、コードの並べ替えはここでも同じです。

例えば:

int i = 0;//1
int j = 1;//2
int a = i+j;//3

たとえば、次の例では、123の順序で実行する必要がありますか?
必ずしも高速である場合は、213オーダーを使用でき、現時点では結果は変わりません。

それならあなたは尋ねなければならないかもしれません、なぜここで312できないのですか?
3は12に依存するため、非常に単純です。12は3の前に実行する必要があり、12の相対的な順序は重要ではありません。
簡単にわかりますが、JVMを決定する方法は?

定義によって決定されるのは、ルールの前に発生します。

ルールの前に起こります

これは事前定義されたルールであり、jvmが最適化(並べ替え)されたときに違反することはできません。

  1. プログラム順序の原則:スレッドでは、プログラムコードシーケンスに従って、前に書かれた操作が後ろに書かれた操作の前に発生します。
  2. 揮発性ルール:揮発性変数の書き込みは読み取りの前に行われるため、揮発性変数の可視性が保証されます。
  3. ロックルール:ロック解除(ロック解除)は、後続のロック(ロック)の前に行う必要があります。
  4. 推移性:AはBに先行し、BはCに先行するため、AはCに先行する必要があります。
  5. スレッドのstartメソッドは、実行するすべてのアクションの前にあります。
  6. スレッドのすべての操作は、スレッドの終了に先行します。
  7. スレッドの割り込み(interrupt())は、割り込みコードに先行します。
  8. オブジェクトのコンストラクターは、finalizeメソッドの前に終了します。

最初のルールプログラムの順序ルールでは、スレッドではすべての操作が順番に実行されますが、JMMでは、実行結果が同じである限り、並べ替えが許可されます。スレッドの実行結果が得られますが、マルチスレッドについても同じことが当てはまるという保証はありません。2番目のルール監視ルールは実際には理解しやすいです。つまり、ロックする前に、ロックを続行する前にロックが解除されていることを確認してください。3番目のルールは、説明した揮発性メモリに適用されます。あるスレッドが最初に変数を書き込み、別のスレッドがそれを読み取る場合、書き込み操作は読み取り操作の前に行う必要があります。4番目のルールは、発生前の推移性です。次のいくつかの記事は1つずつ繰り返されることはありません。

単一のスレッドでは、実際の結果は同じままであるため、並べ替えは重要ではありませんが、複数のスレッドでは問題が大きくなります。

たとえば、次の質問:

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
   }
}

スレッドAは最初にwriterメソッドを実行し、スレッドBはその後multiplyメソッドを実行します。
以下に示すように、並べ替えを実行した場合、結果は4である必要はありません。

	线程A		线程B
	2			
				3
				4
	1

ここで、1、2はプログラムシーケンスルールであり、並べ替えることができます。
3と4は相互に依存しているため、3が発生します-4より前に再配置することはできず、問題は再除外されます。
上記の場合、retの結果は0です。予想と同じではありません。
したがって、ここに問題があります。コードの結果が4になることを明確に望んでいます。

volatileキーワードが出てきます

上記の問題を解決するために、主人公が登場します。

まず、最初の質問です。
静的変数iをvolatileに設定します。

	static volatile int i =0;
	两个线程同时执行以下代码,结果会怎样呢?
	线程A执行将i=2;
	线程B读取i值;

このとき、読み取り/書き込み操作はすべてアトミックであるため、スレッドAが最初に書き込みます(作業メモリーに書き込もうとしています。作業メモリーをメインメモリーに更新する2つのステップが1つにまとめられ、すぐに更新されます。ワーキングメモリに書き込んだ後。)
次に、Bスレッドが読み取ります。取得された場合も同様であり、リフレッシュ直後にも取得されます。
最終結果は2です。


2番目の質問の場合:

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
   }
}

ここでは、フラグのみが揮発性として定義されています。
この時点でhbシーケンスを見てください。
writeメソッドでは、次のように説明します
。volatileキーワードは、並べ替えを禁止します。いわゆる禁止は、コードの前の通常の変数操作がそれ自体の前に発生する必要があり、それ自体の後ろに再配置できないことです。同様に、後者はフロント。ここでのvolatileキーワードは、独自の上部領域と下部領域を分離するバリアに相当します。独自の領域を再配置することは私の仕事ではありませんが、領域間をいじることはできません。(実際には、メモリバリアも使用されます。たとえば、このバリアの前のすべての書き込み操作はメインメモリにフラッシュされます。)
したがって、これにより1〜2の順序が制限されます。
同時に、揮発性書き込みhbが読み取られるため、2 -3
同時に3と4依存関係があるので、3-4

したがって、私たちが望むように、1-2-3-4の順序が最終的に実現されました。

これが可視性と秩序の実現です。


原子性はどうですか?
前述の揮発性は、読み取り/書き込みのアトミック性を認識していませんでしたが、なぜアトミックではないのですか?

ここで説明しているアトミック性は、元の値に基づいて値を変更するi ++に関するものです。
これは単純な読み取りまたは書き込みではありません。
ロジックは次のとおりです。

	先读
	修改
	写

複合操作です。Volatileは、この複合操作の原子性を実現できないため、方法がありません。

i = 0の場合;
スレッドAがi ++を実行し、スレッドBもi ++を実行するとします。
どうなるか想像してみてください

	线程A		线程B
	读			
				读
	修改	
				修改
	写			
				写

上記のシーケンスによれば、iは2ではなく1になります。スレッドAがi = 0になると、スレッドBも0を読み取ります。スレッドAが書き込みを終了すると、スレッドBはすでに読み取りを終了しているため、次のようになります。書かれているときも1。ですから、私たちが期待していたものとは異なります。
Volatileはこの問題を解決できません。
(追記:ここでは、CASを介して解決する、またはロックすることを検討できます)

総括する

揮発性を達成

  • 可視性:ワーキングメモリへの書き込みとメインメモリへのワーキングメモリの更新の2つのステップが1つに結合されます。メインメモリがワーキングメモリに更新され、cpuによってワーキングメモリから取得された値も結合されます。 1つであるため、揮発性変数が作成されます。読み取り/書き込みはアトミックであるため、表示されることが保証されます。
    ツーインワン操作を実現する操作は、アセンブリ内のロックプレフィックスであるため、現在のCPUのキャッシュがメモリにフラッシュされると同時に、他のCPUが無効になるため、他のCPUが再取得する必要があります。キャッシュ。(つまり、書き込み直後に更新し、読み取り時にすぐに更新して読み取ります)
  • 順序性:volatileキーワードは命令の並べ替えを禁止し、メモリバリアが使用されます。バリアの前後でコードをどのように再配置するかは問題ではありませんが、前を追うことはできず、最後を使用することはできません。意味的には、メモリバリアの前の書き込みをメモリにフラッシュする必要があります。これにより、メモリバリアの後の読み取りで、前の書き込みの結果を取得できます。(したがって、メモリバリアは新しいパフォーマンスを低下させ、コードを最適化できなくなります)

実装されていません:

  • Atomicity:単一の操作のアトミック性のみが実現されます。たとえば、i ++は最初に読み取り、後で変更し、最後に書き込みます。これは複合操作であるため、アトミック性は保証されません。

参照

並行プログラミングJavaメモリモデル+ volatileキーワード+ HappenBeforeルール
スレッドセーフ(オン)-volatileキーワードを完全に理解する
Javaインタビュアーのお気に入りのvolatileキーワード

おすすめ

転載: blog.csdn.net/qq_34687559/article/details/114329619