Java Concurrentプログラミングシリーズ[未完成]:
•Java並行性プログラミング:コア理論
•Java Concurrentプログラミング:同期およびその実装原則
•Java Concurrentプログラミング:同期された基礎となる最適化(軽量ロック、バイアスロック)
•Java Concurrentプログラミング:スレッド間のコラボレーション(待機/通知/睡眠/収量/参加)
•Java Concurrentプログラミング:揮発性の使用とその原則
1。揮発性の役割
「Java Concurrencyプログラミング:コア理論」という記事で、可視性、秩序性、原子性の問題について言及しました。通常、同期されたキーワードを使用してこれらの問題を解決できます。ただし、同期の原則を理解している場合は、同期化されたものは比較的ヘビー級操作であり、システムのパフォーマンスに比較的大きな影響を与えることを知っておく必要があります。したがって、他のソリューションがある場合、通常、同期して問題を解決することを避けます。揮発性キーワードは、視界と秩序の問題を解決するためにJavaで提供される別のソリューションです。原子性に関しては、誰もが誤解を招く傾向があります。揮発性変数の単一の読み取り/書き込み操作は、長い二重型変数などの原子性を確保できますが、I ++操作の原子性を保証することはできません。
2。揮発性の使用
揮発性の使用に関しては、いくつかの例を使用して、その使用とシナリオを説明できます。
1.並べ替えを防ぎます
最も古典的な例の1つから、並べ替えの問題を分析しましょう。誰もがシングルトンモデルの実装に精通している必要があり、同時環境では、通常、ダブルチェックロック(DCL)メソッドを使用して実装できます。ソースコードは次のとおりです。
パッケージcom.paddx.test.concurrent; public class singleton {public static volatile singleton singleton; / ***コンストラクターはプライベートで、外部インスタンス化を禁止しています*/ private singleton(){}; public static singleton getInstance(){if(singleton == null){synchronized(singleton){if(singleton == null){singleton = new singleton(); }} singletonを返します。 }}次に、可変シングルトン間に揮発性キーワードを追加する必要がある理由を分析しましょう。この問題を理解するには、まずオブジェクトの構築プロセスを理解する必要があります。オブジェクトをインスタンス化することは、実際には3つのステップに分割できます。
(1)メモリスペースを割り当てます。
(2)オブジェクトを初期化します。
(3)メモリスペースのアドレスを対応する参照に割り当てます。
ただし、オペレーティングシステムは指示を再注文できるため、上記のプロセスも次のプロセスになる可能性があります。
(1)メモリスペースを割り当てます。
(2)メモリスペースのアドレスを対応する参照に割り当てます。
(3)オブジェクトを初期化します
このプロセスがプロセスである場合、非初期化されたオブジェクト参照がマルチスレッド環境で公開される可能性があり、その結果、予測不可能な結果が得られます。したがって、このプロセスの並べ替えを防ぐには、変数を揮発性の変数に設定する必要があります。
2。可視性を達成します
可視性の問題は、主に1つのスレッドが共有変数値を変更することを指し、もう1つのスレッドでは表示できません。可視性の問題の主な理由は、各スレッドに独自のキャッシュ領域 - スレッドワーキングメモリがあることです。揮発性キーワードは、この問題を効果的に解決できます。次の例を見て、その機能を知りましょう。
パッケージcom.paddx.test.concurrent; public class volatiletest {int a = 1; int b = 2; public void change(){a = 3; b = a; } public void print(){system.out.println( "b ="+b+"; a ="+a); } public static void main(string [] args){while(true){final volatileTest test = new volatileTest();新しいスレッド(new runnable(){@override public void run(){try {shood.sleep(10);} catch(arturnedexception e){e.printstacktrace();} test.change();}})。start(); start(); new Sthrea(new runnable(){@Override public void run(){try {shood.sleep(10);} catch(arturnedexception e){e.printstacktrace();} test.print();}})。start(); start(); }}}直感的に言えば、このコードには2つの可能な結果しかありません:b = 3; a = 3またはb = 2; a = 1。ただし、上記のコードを実行して(おそらくもう少し時間がかかるかもしれません)、前の2つの結果に加えて、3番目の結果もあります。
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ..
b = 3; a = 1などの結果が表示されるのはなぜですか?通常の状況では、最初に変更方法を実行してから印刷メソッドを実行する場合、出力の結果はb = 3; a = 3でなければなりません。それどころか、最初に印刷メソッドを実行してから変更方法を実行する場合、結果はb = 2; a = 1にする必要があります。では、b = 3; a = 1の結果はどのように出てきますか?その理由は、最初のスレッドが値a = 3を変更しますが、2番目のスレッドには見えないため、この結果が発生します。 aとbの両方が揮発性タイプの変数に変更されて実行された場合、b = 3; a = 1の結果は二度と表示されません。
3.原子性を確保します
原子性の問題は上記で説明されています。揮発性は、単一の読み取り/書き込みの原子のみを保証できます。この問題は、JLSで説明できます。
17.7 Javaプログラミング言語メモリモデルの目的のために二重および長期の非原子的処理、不揮発性の長いまたは二重値への単一の書き込みは、2つの別々の書き込みとして扱われます。1つは32ビット半分です。これにより、スレッドが1つの書き込みから64ビット値の最初の32ビットを表示し、別の書き込みから2番目の32ビットを表示する状況になります。参照の書き込みと読み取りは、32ビット値または64ビット値として実装されているかどうかに関係なく、常に原子的です。一部の実装では、64ビットの長さまたは2倍の値に単一の書き込みアクションを隣接する32ビット値の2つの書き込みアクションに分割するのが便利な場合があります。効率のために、この動作は実装固有です。 Java Virtual Machineの実装は、原子的または2つの部分で長い値と2倍の値に自由に書き込みを実行できます。 Java仮想マシンの実装は、可能であれば64ビット値の分割を避けるために推奨されます。プログラマーは、共有された64ビットの値を揮発性として宣言するか、プログラムを正しく同期していると宣言することをお勧めします。
この箇所の内容は、前述のものとほぼ似ています。長いダブルとダブルの2つのデータタイプの操作は、高32ビットと低32ビットの2つの部分に分割できるため、通常の長いまたはダブルタイプは原子ではない場合があります。したがって、誰もが共有された長い二重変数を揮発性タイプに設定することを奨励されています。これにより、いずれにせよ、長いダブルとダブルの単一の読み取り/書き込み操作がアトミックであることを保証できます。
揮発性変数が原子性を保証する問題があり、これは簡単に誤解されています。次に、次のプログラムを通じてこの問題を示します。
パッケージcom.paddx.test.concurrent; public class volatiletest01 {volatile int i; public void addi(){i ++; } public static void main(string [] args)throws arturtedexception {final volatiletest01 test01 = new volatileTest01(); for(int n = 0; n <1000; n ++){newスレッド(new runnable(){@override public void run(){try {shood.sleep(10);} catch(arturtedexception e){e.printstacktrace();} test01.addi();})。 } thread.sleep(10000); //上記のプログラムの実行がsystem.out.println(test01.i)が完了するように10秒間待ちます。 }}変数Iに揮発性のあるキーワードを追加した後、このプログラムはスレッドセーフであると誤って信じることができます。上記のプログラムを実行してみてください。これが私のローカルランの結果です:
たぶん誰もが結果を異なって実行します。ただし、揮発性は原子性を保証できないことがわかるはずです(そうでなければ、結果は1000でなければなりません)。その理由も非常に簡単です。 I ++は、実際には3つのステップを含む複合操作です。
(1)iの値を読み取ります。
(2)iに1を追加します。
(3)Iの値をメモリに戻します。
これらの3つの操作が原子的であるという保証はありません。 AtomicIntegerまたは同期して+1操作の原子性を確保できます。
注:スレッド。Sleep()メソッドは、コードの上記のセクションの多くの場所で実行され、並行性の問題の可能性を高め、他の効果はありません。
3。揮発性の原理
上記の例を通して、基本的に揮発性とそれをどのように使用するかを知っている必要があります。それでは、揮発性の基礎層がどのように実装されているかを見てみましょう。
1。可視性の実装:
前の記事で述べたように、スレッド自体はメインメモリデータと直接相互作用するのではなく、スレッドのワーキングメモリを介して対応する操作を完了します。これは、スレッド間のデータが見えない重要な理由でもあります。したがって、揮発性変数の可視性を実現するために、この側面から直接開始できます。揮発性変数と通常の変数に関する操作の書き込みには、2つの主な違いがあります。
(1)揮発性変数を変更すると、変更された値はメインメモリを更新することを余儀なくされます。
(2)揮発性変数を変更すると、他のスレッドのワーキングメモリに対応する変数値が失敗します。したがって、この変数の値をもう一度読み取るときは、メインメモリの値を再度読み取る必要があります。
これら2つの操作により、揮発性変数の視認性の問題を解決できます。
2。秩序ある実装:
この問題を説明する前に、まずJavaの事前のルールを理解しましょう。 JSR 133の前にたまたまの定義は次のとおりです。
2つのアクションを実行することで順序付けることができます。これまでの関係は、1つのアクションが別のアクションよりも先に行われた場合、1つ目は2番目の前に表示され、注文されます。
素人の用語では、b前に起こった場合、aはbに表示されます。 (誰もがこれを覚えておく必要があります。なぜなら、その言葉は前後に簡単に誤解されるからです)。 JSR 133で定義されているルールの前に、何が起こるかを見てみましょう。
•スレッド内の各アクションは、そのスレッドの後続のすべてのアクションの前に発生します。 •モニターでのロック解除は、そのモニターの後続のすべてのロックの前に発生します。 •揮発性フィールドへの書き込みは、その後の揮発性を読むたびに発生します。 •スレッドでstart()を開始する()スレッドのアクションが開始される前に呼び出されます。 •スレッド内のすべてのアクションは、他のスレッドがそのスレッドのJoin()から正常に戻る前に発生します。 •アクションAがアクションBの前に発生し、bがアクションCの前に発生する場合、aはcの前に発生します。
翻訳:
•以前の操作は、同じスレッドで行われます。 (つまり、単一のスレッドでは、コードの順序で実行することが合法です。ただし、コンパイラとプロセッサは、単一のスレッド環境で実行結果に影響を与えることなく並べ替えることができます。つまり、これはルールがコンパイルの並べ替えと命令の再注文を保証できないということです)。
•モニターでの操作のロック解除 - その後のロック操作の前に行われます。 (同期されたルール)
•揮発性変数への操作の書き込み - その後の読み取り操作の前に。 (揮発性ルール)
•スレッドのstart()メソッドは、スレッドの後続のすべての操作の前に発生します。 (スレッドスタートルール)
•スレッドのすべての操作は、他のスレッドの前に発生します。このスレッドに参加して、成功した操作を返します。
•AがB前に発生する場合、BはCの前に発生します。
ここでは、主に3番目のルール:揮発性変数の秩序性を確保するためのルールを調べます。記事「Java Concurrencyプログラミング:Core Theory」は、並べ替えがコンパイラの再注文とプロセッサの並べ替えに分割されていることを述べました。揮発性メモリセマンティクスを実装するために、JMMはこれら2つのタイプの揮発性変数の並べ替えを制限します。以下は、揮発性変数についてJMMによって指定されたルールを並べ替える表です。
| 並べ替えることができます | 第2操作 | |||
| 第1操作 | 通常の負荷 通常の店 | 揮発性負荷 | 揮発性ストア | |
| 通常の負荷 通常の店 | いいえ | |||
| 揮発性負荷 | いいえ | いいえ | いいえ | |
| 揮発性ストア | いいえ | いいえ | ||
3。メモリバリア
揮発性の可視性を実装し、セマンティクスのために実装するため。基礎となるJVMは、「メモリバリア」と呼ばれるものを通じて行われます。メモリフェンスとも呼ばれるメモリバリアは、メモリ操作に連続的な制限を実装するために使用されるプロセッサ命令のセットです。上記のルールを完了するために必要なメモリバリアは次のとおりです。
| 必要な障壁 | 第2操作 | |||
| 第1操作 | 通常の負荷 | 通常の店 | 揮発性負荷 | 揮発性ストア |
| 通常の負荷 | ロードストア | |||
| 通常の店 | Storestore | |||
| 揮発性負荷 | ロードロード | ロードストア | ロードロード | ロードストア |
| 揮発性ストア | Storeload | Storestore | ||
(1)ロードロードバリア
実行順序:load1 - > loadload - > load2
Load2および後続のロード命令が、データをロードする前にLoad1でロードされたデータにアクセスできることを確認してください。
(2)Storestoreバリア
実行注文:store1 - > storestore - > store2
Store2および後続のStore命令が実行される前に、Store1操作のデータが他のプロセッサに表示されることを確認してください。
(3)ロードストアバリア
実行順序:load1 - > loadstore - > store2
Store2および後続のStore命令が実行される前に、Load1でロードされたデータにアクセスできることを確認してください。
(4)ストアロードバリア
実行注文:store1 - > storeload - > load2
Load2および後続のロード命令が読み取られる前に、Store1のデータが他のプロセッサに表示されることを確認してください。
最後に、例を使用して、JVMにメモリバリアがどのように挿入されるかを説明できます。
パッケージcom.paddx.test.concurrent; public class memorybarrier {int a、b;揮発性int V、u; void f(){int i、j; i = a; j = b; i = v; //ロードロードj = u; // loadstore a = i; b = j; // Storestore v = i; // storestore u = j; // storeLoad i = u; // LoadLoad // LoadStore J = B; a = i; }}4。概要
全体として、揮発性を理解することは依然として比較的困難です。特によくわからない場合は、急ぐ必要はありません。それを完全に理解するにはプロセスが必要です。また、後続の記事では、揮発性の使用シナリオも何度も表示されます。ここでは、揮発性と元の知識に関する基本的な知識についての基本的な理解があります。一般的に言えば、揮発性は同時プログラミングの最適化であり、いくつかのシナリオで同期したものに取って代わることができます。ただし、揮発性は同期の位置を完全に置き換えることはできません。一部の特別なシナリオでのみ、揮発性を適用できます。一般に、次の2つの条件を同時に満たす必要があります。
(1)変数への書き込み操作は、現在の値に依存しません。
(2)この変数は、他の変数との不変に含まれていません。
Java Concurrentプログラミングに関する上記の記事:Volatileの使用とその原理分析は、私があなたと共有するすべてのコンテンツです。参照を提供できることを願っています。wulin.comをもっとサポートできることを願っています。