多くの友人は、キーワードが不安定であると聞いたことがあり、それを使用したかもしれません。 Java 5以前は、プログラムでそれを使用すると予想外の結果が得られるため、物議を醸すキーワードでした。 Java 5が揮発性のキーワードがその活力を取り戻した後にのみ。
揮発性のキーワードは文字通り理解するのが簡単ですが、うまく使用するのは簡単ではありません。揮発性キーワードはJavaのメモリモデルに関連しているため、揮発性キーを伝える前に、まずメモリモデルに関連する概念と知識を理解し、次に揮発性キーワードの実装原則を分析し、最後に揮発性キーワードを使用するいくつかのシナリオを提供します。
この記事のディレクトリの概要は次のとおりです。
1。メモリモデルの関連概念
誰もが知っているように、コンピューターがプログラムを実行すると、各命令はCPUで実行され、命令の実行中に、必然的にデータの読み書きが含まれます。プログラム操作中の一時データはメインメモリ(物理メモリ)に保存されるため、現時点では問題があります。 CPUの実行速度は非常に高速であるため、メモリからデータを読み取り、メモリに執筆するプロセスは、CPUの指示の実行よりもはるかに遅くなります。したがって、データ操作をいつでもメモリとの相互作用を通じて実行する必要がある場合、命令実行速度は大幅に低下します。したがって、CPUにはキャッシュがあります。
つまり、プログラムが実行されている場合、操作に必要なデータをメインメモリからCPUのキャッシュにコピーします。次に、CPUが計算を実行すると、キャッシュからデータを直接読み取り、データを書き込むことができます。操作が完了すると、キャッシュ内のデータがメインメモリに流されます。次のコードなど、簡単な例を示しましょう。
i = i + 1;
スレッドがこのステートメントを実行すると、最初にメインメモリからiの値を読み取り、次にコピーをキャッシュにコピーし、次にCPUが1に1を追加してIに追加し、キャッシュにデータを書き込み、最終的にメインメモリにキャッシュ内の最新値を更新します。
このコードが単一のスレッドで実行されていることに問題はありませんが、マルチスレッドで実行されると問題が発生します。マルチコアCPUでは、各スレッドが異なるCPUで実行される場合があるため、各スレッドには実行時に独自のキャッシュがあります(シングルコアCPUの場合、この問題は実際に発生しますが、スレッドスケジューリングの形式で個別に実行されます)。この記事では、マルチコアCPUを例として取り上げます。
たとえば、2つのスレッドが同時にこのコードを実行します。 Iの値が最初に0の場合、2つのスレッドが実行された後、Iの値が2になることを願っています。しかし、これは当てはまりますか?
次の状況のいずれかがある場合があります。最初に、2つのスレッドがIの値を読み取り、それぞれのCPUのキャッシュに保存し、スレッド1が1の操作を実行し、Iの最新値をメモリに書き込みます。この時点で、スレッド2のキャッシュ内のIの値はまだ0です。1操作を実行した後、Iの値は1で、スレッド2はIの値をメモリに書き込みます。
最終結果Iは2ではなく1です。これは有名なキャッシュの一貫性の問題です。複数のスレッドでアクセスされるこの変数は、通常、共有変数と呼ばれます。
つまり、変数が複数のCPUでキャッシュされている場合(通常、マルチスレッドプログラミング中にのみ発生する)、キャッシュの矛盾の問題があるかもしれません。
キャッシュの不一致の問題を解決するために、通常、2つの解決策があります。
1)バスにロック#ロックを追加します
2)キャッシュコヒーレンスプロトコルを介して
これらの2つの方法は、ハードウェアレベルで提供されます。
初期のCPUでは、バスにロック#ロックを追加することにより、キャッシュの矛盾の問題が解決されました。 CPUと他のコンポーネント間の通信はバスを介して実行されるため、バスがロック#ロックで追加されている場合、他のCPUが他のコンポーネント(メモリなど)にアクセスすることをブロックしているため、1つのCPUのみがこの変数のメモリを使用できます。たとえば、上記の例では、スレッドがi = i +1を実行している場合、およびこのコードの実行中にLCOK#ロック信号がバスに送信された場合、コードが完全に実行されるのを待ってからのみ、他のCPUは変数Iが配置されているメモリから変数を読み取り、対応する操作を実行できます。これにより、キャッシュの矛盾の問題が解決します。
しかし、他のCPUがバスのロック中にメモリにアクセスできず、非効率性をもたらすため、上記の方法には問題があります。
したがって、キャッシュの一貫性プロトコルが出現します。最も有名なものはIntelのMesiプロトコルであり、各キャッシュで使用される共有変数のコピーが一貫していることを保証します。その中心的なアイデアは、CPUがデータを書き込む場合、動作する変数が共有変数であることがわかった場合、つまり他のCPUに変数のコピーがあることが、他のCPUが変数のキャッシュラインを無効な状態に設定するように信号します。したがって、他のCPUがこの変数を読み取り、キャッシュの変数をキャッシュするキャッシュラインが無効であることを発見する必要がある場合、メモリから読み直します。
2。同時プログラミングの3つの概念
同時プログラミングでは、通常、次の3つの問題が発生します。原子性の問題、可視性の問題、秩序ある問題です。最初にこれらの3つの概念を見てみましょう。
1。原子性
Atomicity:つまり、1つの操作または複数の操作がすべて実行され、実行のプロセスがいかなる要因によっても中断されないか、実行されません。
非常に古典的な例は、銀行口座の転送の問題です。
たとえば、アカウントAからアカウントBに1,000元転送すると、必然的に2つの操作が含まれます。アカウントAから1,000元を減算し、1,000元をアカウントBに追加します。
これらの2つの操作が原子的でない場合、どのような結果が生じるか想像してください。アカウントAから1,000元を差し引いた場合、操作は突然終了します。その後、500元がBから撤回され、500元を引き出した後、1,000元を追加してBに操作しました。これは、アカウントAに1,000元をマイナスしているが、アカウントBが1,000元を受け取っていないという事実につながります。
したがって、これらの2つの操作は、予期しない問題がないことを確認するために原子でなければなりません。
同時プログラミングに反映される結果は何ですか?
最も簡単な例を挙げると、32ビット変数を割り当てるプロセスがアトミックではない場合はどうなりますか?
i = 9;
スレッドがこのステートメントを実行した場合、32ビット変数の割り当てには2つのプロセスが含まれていると仮定します。16ビットの低い割り当てとより高い16ビットの割り当てです。
その後、状況が発生する可能性があります。16ビット値が低い場合、それは突然中断され、この時点で別のスレッドがiの値を読み取ると、読み取られたのは間違ったデータです。
2。可視性
可視性とは、複数のスレッドが同じ変数にアクセスし、1つのスレッドが変数の値を変更し、他のスレッドがすぐに変更された値を確認できることを指します。
簡単な例については、次のコードを参照してください。
//スレッド1によって実行されるコードはint i = 0; i = 10; //スレッド2で実行されたコードはj = iです。
実行スレッド1がCPU1で、実行スレッド2がCPU2の場合。上記の分析から、スレッド1が文I = 10を実行すると、Iの初期値がCPU1のキャッシュにロードされ、10の値が割り当てられることがわかります。CPU1のキャッシュのIの値は10になりますが、メインメモリにすぐに書き込まれません。
この時点で、スレッド2はj = iを実行し、最初にメインメモリに移動してiの値を読み取り、CPU2のキャッシュにロードします。メモリ内のiの値はまだ0であるため、jの値は10ではなく0になることに注意してください。
これが可視性の問題です。スレッド1が変数Iを変更した後、スレッド2はスレッド1によって変更された値をすぐには確認しません。
3。注文
注文:つまり、プログラムの実行順序はコードの順序で実行されます。簡単な例については、次のコードを参照してください。
int i = 0;ブールフラグ= false; i = 1; //ステートメント1 flag = true; //ステートメント2
上記のコードは、int-type変数であるブール型変数を定義し、それぞれ2つの変数に値を割り当てます。コードシーケンスの観点から、ステートメント1はステートメント2の前です。したがって、JVMが実際にこのコードを実行する場合、ステートメント1がステートメント2の前に実行されることを保証しますか?必ずしもそうではありません、なぜですか?ここで命令の並べ替えが発生する場合があります。
指示の並べ替えとは何かを説明しましょう。一般的に、プログラムの操作効率を改善するために、プロセッサは入力コードを最適化する場合があります。プログラム内の各ステートメントの実行順序がコードの順序と一致することを保証するものではありませんが、プログラムの最終実行結果とコード実行シーケンスの結果が一貫していることを保証します。
たとえば、ステートメント1とステートメント2を実行する上記のコードでは、最初に最終プログラムの結果に影響を与えません。次に、実行プロセス中にステートメント2が最初に実行され、ステートメント1が後で実行される可能性があります。
ただし、プロセッサは指示を再注文しますが、プログラムの最終結果がコード実行シーケンスと同じであることを保証することに注意してください。それで、それが何であるかは何ですか?次の例を見てみましょう。
int a = 10; //ステートメント1int r = 2; //ステートメント2a = a + 3; //ステートメント3r = a*a; //ステートメント4
このコードには4つのステートメントがあるため、実行可能な順序は次のとおりです。
そのため、実行命令になることは可能ですか:ステートメント2ステートメント1ステートメント4ステートメント3
プロセッサは、並べ替え時に命令間のデータ依存性を考慮するため、不可能です。命令命令2が命令1の結果を使用する必要がある場合、プロセッサは命令1の前に命令1が実行されることを確認します。
並べ替えは、単一のスレッド内でプログラム実行の結果に影響しませんが、マルチスレッドについてはどうですか?以下の例を見てみましょう。
//スレッド1:context = loadcontext(); // state 1inited = true; // State 2 //スレッド2:while(!inited){sleep()} dosomethingwithconfig(context);上記のコードでは、ステートメント1と2にはデータの依存関係がないため、再注文される場合があります。並べ替えが発生した場合、ステートメント2は最初にスレッド1の実行中に実行されます。これは、スレッド2が初期化作業が完了したと考え、その後、whileループから飛び出して、dosomething -withconfig(コンテキスト)メソッドを実行します。現時点では、コンテキストが初期化されていないため、プログラムエラーが発生します。
上記からわかるように、命令の並べ替えは単一のスレッドの実行に影響を与えませんが、スレッドの同時実行の正確性に影響します。
言い換えれば、同時プログラムを正しく実行するには、原子性、可視性、順序性を確保する必要があります。保証されていない限り、プログラムが誤って実行される可能性があります。
3.Javaメモリモデル
メモリモデルと同時プログラミングで発生する可能性のあるいくつかの問題について話しました。 Javaメモリモデルを見て、Javaメモリモデルが当社に提供するものを保証するものと、マルチスレッドプログラミングを実行するときにプログラム実行の正しさを確保するためにJavaで提供される方法とメカニズムを保証しましょう。
Java仮想マシンの仕様では、Javaメモリモデル(JMM)を定義して、さまざまなハードウェアプラットフォームとオペレーティングシステム間のメモリアクセスの違いをブロックして、Javaプログラムがさまざまなプラットフォームで一貫したメモリアクセス効果を実現できるようにします。では、Javaメモリモデルは何を規定していますか?プログラム内の変数のアクセスルールを定義します。より広く言うと、プログラムの実行の順序を定義します。より良い実行パフォーマンスを得るために、Javaメモリモデルは、実行エンジンがプロセッサのレジスタまたはキャッシュを使用して命令実行速度を改善することを制限しておらず、コンパイラが指示を並べ替えることも制限しないことに注意してください。言い換えれば、Javaメモリモデルでは、キャッシュの一貫性の問題と命令の並べ替えの問題もあります。
Javaメモリモデルは、すべての変数がメインメモリ(上記の物理メモリと同様)にあることを規定しており、各スレッドには独自の作業メモリ(前のキャッシュと同様)があります。変数上のスレッドのすべての操作は、ワーキングメモリで実行する必要があり、メインメモリで直接動作することはできません。また、各スレッドは他のスレッドの作業メモリにアクセスできません。
簡単な例を示すには:Javaで、次のステートメントを実行してください。
i = 10;
実行スレッドは、まず、変数Iが独自の作業スレッドにあるキャッシュラインを割り当て、次にメインメモリに書き込む必要があります。値10をメインメモリに直接書き込む代わりに。
それでは、Java言語自体が原子性、可視性、秩序を保証するものは何ですか?
1。原子性
Javaでは、基本データ型の変数の読み取りおよび割り当て操作は原子操作です。つまり、これらの操作を中断することはできず、実行しないかどうかです。
上記の文は単純に思えますが、理解するのはそれほど簡単ではありません。次の例を参照してください:
次の操作のどれが原子操作であるかを分析してください。
x = 10; //ステートメント1y = x; //ステートメント2x ++; //ステートメント3x = x + 1; //ステートメント4
一見すると、一部の友人は、上記の4つのステートメントの操作はすべて原子操作であると言うかもしれません。実際、ステートメント1のみが原子操作であり、他の3つのステートメントは原子操作ではありません。
ステートメント1は値10をxに直接割り当てます。つまり、スレッドはこのステートメントを実行し、値10をワーキングメモリに直接書き込みます。
ステートメント2には、実際には2つの操作が含まれています。最初にxの値を読み取り、次にxの値をワーキングメモリに書き込む必要があります。 Xの値を読み取り、ワーキングメモリにXの値を書き込む2つの操作は原子動作ですが、それらは一緒に原子操作ではありません。
同様に、x ++およびx = x+1に含まれる3操作:xの値を読み取り、1の操作を実行し、新しい値を書き込みます。
したがって、上記の4つのステートメントにおけるステートメント1の操作のみが原子です。
言い換えれば、単純な読み取りと割り当てのみ(および変数に割り当てる必要があり、変数間の相互割り当ては原子操作ではありません)は原子動作です。
ただし、ここに注意すべきことが1つあります。32ビットプラットフォームでは、64ビットデータの読み取りと割り当てを2つの操作で完了する必要があり、その原子性を保証することはできません。ただし、最新のJDKでは、JVMは64ビットデータの読み取りと割り当ても原子動作であることを保証したようです。
上記から、Javaメモリモデルは、基本的な読み取りと割り当てが原子操作であることのみを保証することがわかります。より広い範囲の操作の原子性を達成したい場合は、同期してロックして達成できます。同期とロックは、1つのスレッドのみがいつでもコードブロックを実行することを保証できるため、自然に原子性の問題はありません。
2。可視性
可視性のために、Javaは視認性を確保するための揮発性キーワードを提供します。
共有変数が揮発性によって変更されると、変更された値がすぐにメインメモリに更新され、他のスレッドが読み取る必要がある場合、メモリ内の新しい値が読み取られます。
ただし、通常の共有変数は可視性を保証することはできません。これは、通常の共有変数が変更された後にメインメモリに書き込まれる場合が不確実であるためです。他のスレッドがそれを読んだ場合、元の古い値がまだメモリにある可能性があるため、視界を保証することはできません。
さらに、同期とロックは視認性を確保することもできます。同期とロックにより、1つのスレッドのみが同時にロックを取得し、同期コードを実行することを保証できます。ロックを解放する前に、変数の変更がメインメモリに更新されます。したがって、可視性を保証できます。
3。注文
Javaメモリモデルでは、コンパイラとプロセッサは命令を再注文することが許可されていますが、再注文プロセスはシングルスレッドプログラムの実行に影響しませんが、マルチスレッドの同時実行の正確性に影響します。
Javaでは、揮発性キーワードを介して特定の「オーダーライン」を確保できます(次のセクションでは、特定の原則について説明します)。さらに、同期とロックを使用して順序を確保できます。明らかに、同期してロックすると、各瞬間に同期コードを実行するスレッドがあることを確認します。これは、スレッドが同期コードを順番に実行させることに相当し、順序を自然に保証します。
さらに、Javaメモリモデルには、いくつかの生得的な「オーダーライン」があります。つまり、通常は実現前の原則と呼ばれる手段なしで保証できます。 2つの操作の実行命令を実現前の原則から導き出すことができない場合、秩序性を保証することはできず、仮想マシンはそれらを自由に並べ替えることができます。
前の原則(優先発生の原則)を紹介しましょう。
これらの8つの原則は、「Java仮想マシンの詳細な理解」から抜粋されています。
これらの8つのルールの中で、最初の4つのルールがより重要ですが、最後の4つのルールはすべて明らかです。
以下の最初の4つのルールを説明しましょう。
プログラムの注文規則については、私の理解では、プログラムコードの実行が単一のスレッドで注文されるように見えることです。このルールは、「前面に記述された操作は、背面に書かれた操作で最初に発生する」と述べているが、これは、仮想マシンが指示されたプログラムコードを再注文する可能性があるため、プログラムがコードシーケンスで実行されるように見える順序である必要があることに注意してください。並べ替えは実行されますが、最終的な実行結果はプログラムのシーケンシャル実行と一致しており、データ依存関係がない命令のみを並べ替えます。したがって、単一のスレッドでは、プログラムの実行は秩序ある方法で実行されるように見えます。これは注意して理解する必要があります。実際、このルールは、単一のスレッドでプログラムの実行結果の正しさを確保するために使用されますが、マルチスレッドの方法でプログラムの正しさを保証することはできません。
2番目のルールも理解しやすいです。つまり、同じロックがロック状態にある場合、ロック操作を継続する前にリリースする必要があります。
3番目のルールは比較的重要なルールであり、後で説明するものでもあります。直感的には、スレッドが最初に変数を書き込み、次にスレッドが読み取る場合、読み取り操作で最初に書き込み操作が発生します。
4番目のルールは、実際には、前に起こる原則が推移的であることを反映しています。
4。揮発性キーワードの詳細な分析
私は以前に多くのことについて話しましたが、それらは実際に揮発性のキーワードを伝える方法を開いているので、トピックに到達しましょう。
1.揮発性キーワードの2層セマンティクス
共有変数(クラスメンバー変数、クラスの静的メンバー変数)が揮発性によって変更されると、2層のセマンティクスがあります。
1)この変数を操作するときに異なるスレッドの可視性、つまり1つのスレッドが特定の変数の値を変更することを確認し、この新しい値はすぐに他のスレッドに表示されます。
2)指示を並べ替えることは禁止されています。
最初にコードを見てみましょう。スレッド1が最初に実行され、スレッド2が後で実行される場合:
//スレッド1boolean stop = false; while(!stop){dosomething();} //スレッド2stop = true;このコードは非常に典型的なコードであり、多くの人がスレッドを中断するときにこのマークアップ方法を使用する場合があります。しかし、実際、このコードは完全に正しく実行されますか?スレッドは中断されますか?必ずしもそうではありません。おそらく、ほとんどの場合、このコードはスレッドを中断する可能性がありますが、スレッドが中断されない可能性もあります(この可能性は非常に小さくなりますが、これが起こると、死んだループが発生します)。
このコードがスレッドを中断しない理由を説明しましょう。前述のように、各スレッドには操作中に独自の作業メモリがあるため、スレッド1が実行されているときに、停止変数の値をコピーし、独自の作業メモリに配置します。
次に、Thread 2がSTOP変数の値を変更したが、メインメモリに書き込む時間がなかった場合、Thread 2は他のことを実行するために、スレッド1はスレッド2の停止変数への変更について知らないため、ループし続けます。
しかし、揮発性で変更した後、それは異なります:
最初:揮発性キーワードを使用すると、変更された値がメインメモリにすぐに書き込まれます。
2番目:揮発性キーワードを使用すると、スレッド2が変更すると、スレッド1のワーキングメモリのキャッシュ変数ストップのキャッシュラインが無効になります(ハードウェア層に反映されると、CPUのL1またはL2キャッシュの対応するキャッシュラインが無効です)。
3番目:スレッド1のワーキングメモリのキャッシュ変数の停止のキャッシュラインは無効であるため、スレッド1は、変数ストップの値を再び読み取るとメインメモリで読み取ります。
次に、スレッド2が停止値を変更すると(もちろん、ここには2つの操作があり、スレッド2のワーキングメモリの値を変更し、メモリに変更された値を書き込みます)、スレッド1のワーキングメモリのキャッシュ変数ストップのキャッシュラインは無効になります。スレッド1が読み取ると、キャッシュラインが無効であることがわかります。キャッシュラインの対応するメインメモリアドレスが更新されるのを待ち、対応するメインメモリの最新値を読み取ります。
次に、スレッド1が読むものは最新の正しい値です。
2。揮発性は原子性を保証しますか?
上記から、揮発性キーワードが操作の可視性を保証することを知っていますが、変数の操作が原子的であることを揮発性が確実にすることができますか?
以下の例を見てみましょう。
パブリッククラステスト{public volatile int inc = 0; public void crossion(){inc ++; } public static void main(string [] args){最終テスト= new test(); for(int i = 0; i <10; i ++){new shood(){public void run(){for(int j = 0; j <1000; j ++)test.increase(); }; }。始める(); } while(thread.activecount()> 1)//以前のスレッドがthread.yield()を完了していることを確認してください。 System.out.println(test.inc); }}このプログラムの出力結果は何ですか?たぶん、何人かの友人はそれが10,000だと思うでしょう。しかし、実際には、各実行の結果は一貫性がなく、10,000未満であることがわかります。
一部の友人は質問があるかもしれません、それは間違っています。上記は、Variable Incで自己充電操作を実行することです。揮発性は可視性を保証するため、各スレッドでのINCの自己障害の後、他のスレッドで変更された値を見ることができます。したがって、10個のスレッドがそれぞれ1000操作を実行しているため、Incの最終値は1000*10 = 10000でなければなりません。
ここには誤解があります。揮発性キーワードは可視性を確保できますが、上記のプログラムは原子性を保証できないため間違っています。可視性は、最新の値が毎回読み取られることを保証できますが、変動は変数の動作の原子性を保証することはできません。
前述のように、自動インクリメント操作は原子ではありません。変数の元の値を読み取り、追加の操作を実行し、ワーキングメモリに書き込むことが含まれます。つまり、自己増加操作の3つのサブ操作は個別に実行される場合があります。これは、次の状況につながる可能性があります。
特定の時間に変数incの値が10の場合、
スレッド1は、変数で自己増加操作を実行します。スレッド1は最初に変数incの元の値を読み取り、次にスレッド1がブロックされます。
次に、スレッド2は変数で自己侵入操作を実行し、スレッド2は変数Incの元の値も読み取ります。スレッド1は変数Incで読み取り操作のみを実行し、変数を変更しないため、スレッド2のCache Inc Cache変数Incのキャッシュラインは無効になりません。したがって、スレッド2はメインメモリに直接移動して、Incの値を読み取ります。 Incの値が10であることがわかった場合、1を追加する操作を実行し、ワーキングメモリに11を書き込み、最終的にメインメモリに書き込みます。
次に、スレッド1が追加操作を実行します。 Incの値は読み取られているため、スレッド1のIncの値は現時点ではまだ10であるため、スレッド1がIncを追加した後、11になり、11を書き込み、ワークメモリに書き込み、最終的にメインメモリに書き込みます。
その後、2つのスレッドが自己侵入操作を実行した後、Incは1だけ増加します。
これを説明した後、一部の友人は質問があるかもしれません、それは間違っています。揮発性変数を変更するときに変数がキャッシュラインを無効にすることを保証しませんか?その後、他のスレッドは新しい値を読み取ります。はい、これは正しいです。これは、上記のルールの前に行われる揮発性変数ルールですが、スレッド1が変数を読み取ってブロックされている場合、INC値は変更されないことに注意する必要があります。その後、揮発性は、スレッド2がメモリから変数incの値を読み取ることを保証できますが、スレッド1はそれを変更していないため、スレッド2は変更された値をまったく表示しません。
根本的な原因は、自動侵入操作が原子動作ではなく、変数が変数の動作が原子であることを保証できないことです。
上記のコードを以下のいずれかに変更することで、効果を達成できます。
同期を使用してください:
パブリッククラステスト{public int inc = 0; public同期void増加(){inc ++; } public static void main(string [] args){最終テスト= new test(); for(int i = 0; i <10; i ++){new shood(){public void run(){for(int j = 0; j <1000; j ++)test.increase(); }; }。始める(); } while(thread.activecount()> 1)//以前のスレッドがthread.yield()を完了していることを確認してください。 System.out.println(test.inc); }}ロックの使用:
パブリッククラステスト{public int inc = 0; lock lock = new ReentrantLock(); public void ression(){lock.lock(); {inc ++; }最後に{lock.unlock(); }} public static void main(string [] args){final test test = new test(); for(int i = 0; i <10; i ++){new shood(){public void run(){for(int j = 0; j <1000; j ++)test.increase(); }; }。始める(); } while(thread.activecount()> 1)//以前のスレッドがshood.yield()を実行されていることを確認してください。 System.out.println(test.inc); }} AtomicIntegerの使用:
パブリッククラステスト{public Atomicinteger inc = new AtomicInteger(); public void ression(){inc.getandincrement(); } public static void main(string [] args){最終テスト= new test(); for(int i = 0; i <10; i ++){new shood(){public void run(){for(int j = 0; j <1000; j ++)test.increase(); }; }。始める(); } while(thread.activecount()> 1)//以前のスレッドがshood.yield()を実行されていることを確認してください。 System.out.println(test.inc); }}いくつかの原子操作クラスは、java.util.concurrent.atomicパッケージのJava 1.5の下で提供されています。つまり、自己圧縮(1操作を追加)、自己決定(1操作を追加)、添加操作(数を追加)、および減算操作(数字を追加)して、これらの操作が原子操作であることを確認します。 AtomicはCASを使用してAtomic操作を実装します(比較とスワップ)。 CASは、実際にプロセッサが提供するCMPXCHG命令を使用して実装されており、プロセッサはCMPXCHG命令を実行することは原子操作です。
3.揮発性は秩序を確保しますか?
前述のように、揮発性キーワードは命令の並べ替えを禁止することができるため、揮発性はある程度秩序を確保できます。
揮発性キーワードの並べ替えを禁じられた2つの意味があります。
1)プログラムが揮発性変数の読み取りまたは書き込み操作を実行する場合、以前の操作のすべての変更が行われている必要があり、結果はすでに後続の操作に表示されています。その後の操作はまだ作成されていないはずです。
2)命令の最適化を実行する場合、揮発性変数にアクセスされたステートメントをその背後に配置することも、揮発性変数に続くステートメントをその前に配置することもできません。
上記で言われていることは少し混乱しているので、簡単な例を挙げてください。
// xとyは非揮発性変数です//フラグは揮発性変数x = 2; //ステートメント1y = 0; //ステートメント2flag = true; //ステートメント3x = 4; //ステートメント4y = -1; //ステートメント5
フラグ変数は揮発性変数であるため、命令の並べ替えプロセスを実行する場合、ステートメント3はステートメント1および2の前に配置されず、ステートメント3およびステートメント4と5の後に配置されません。ただし、ステートメント1およびステートメント4およびステートメント5の順序が保証されないことは保証されていません。
さらに、揮発性キーワードは、ステートメント3が実行されたとき、ステートメント1とステートメント2を実行する必要があり、ステートメント1とステートメント2の実行結果がステートメント3、ステートメント4、およびステートメント5に表示されることを保証できます。
それでは、前の例に戻りましょう。
//スレッド1:context = loadcontext(); // state 1inited = true; // State 2 //スレッド2:while(!inited){sleep()} dosomethingwithconfig(context);この例を挙げたとき、ステートメント2がステートメント1の前に実行される可能性があるため、コンテキストが初期化されない可能性があり、スレッド2が非初期化されたコンテキストを使用して動作させる可能性があると述べ、プログラムエラーが発生します。
initited変数が揮発性キーワードで変更されている場合、この問題は発生しません。これは、ステートメント2が実行されると、コンテキストが初期化されていることを確実に保証するためです。
4.揮発性の原理と実装メカニズム
存在する揮発性キーワードのいくつかの用途の以前の説明。 Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}参考资料:
《Java编程思想》
《深入理解Java虚拟机》
上記はこの記事のすべての内容です。私はそれがすべての人の学習に役立つことを願っています、そして、私は誰もがwulin.comをもっとサポートすることを願っています。