1. メモリ モデルの関連概念
ご存知のとおり、コンピュータがプログラムを実行する際には、CPU で各命令が実行されますが、命令を実行する過程で必然的にデータの読み書きが発生します。プログラム実行中の一時データはメインメモリ(物理メモリ)に保存されるため、CPUの実行速度が非常に速いため、メモリからデータを読み出したり、メモリにデータを書き込む処理が遅くなるという問題があります。命令の実行速度は CPU の速度とは異なり、非常に遅いため、データの操作を常にメモリとの対話を通じて実行する必要がある場合、命令の実行速度は大幅に低下します。つまりCPU内にキャッシュが存在します。
つまり、プログラムの実行中に演算に必要なデータがメインメモリから CPU のキャッシュにコピーされ、CPU は計算時にキャッシュから直接データを読み取って書き込むことができます。 、キャッシュ内のデータがメイン メモリに更新されます。次のコードのような簡単な例を考えてみましょう:
1i = i 1;
スレッドがこのステートメントを実行すると、まずメイン メモリから i の値を読み取り、そのコピーをキャッシュにコピーします。次に、CPU が i を 1 だけインクリメントする命令を実行し、データをキャッシュに書き込みます。最後に値をキャッシュに書き込み、キャッシュ内の i の最新の値がメイン メモリにフラッシュされます。
このコードを単一スレッドで実行する場合は問題ありませんが、複数のスレッドで実行すると問題が発生します。マルチコア CPU では、各スレッドが異なる CPU で実行される可能性があるため、各スレッドは実行時に独自のキャッシュを持ちます (シングルコア CPU の場合、この問題は実際にも発生しますが、スレッドごとにスケジュールされ、個別に実行される形式になります)。 。この記事では、マルチコア CPU を例に取り上げます。
たとえば、このコードを同時に実行する 2 つのスレッドがあるとします。最初の i の値が 0 である場合、2 つのスレッドの実行後に i の値が 2 になることが期待されます。しかし、そうなるのでしょうか?
次のような状況が考えられます: 最初に 2 つのスレッドがそれぞれ i の値を読み取ってそれぞれの CPU のキャッシュに格納し、次にスレッド 1 が 1 を加算して、i の最新の値 1 をメモリに書き込みます。このとき、スレッド 2 のキャッシュ内の i の値はまだ 0 です。1 を加算すると、i の値は 1 になり、スレッド 2 は i の値をメモリに書き込みます。
i の最終的な値は 2 ではなく 1 です。これは有名なキャッシュ整合性の問題です。複数のスレッドによってアクセスされる変数は、通常、共有変数と呼ばれます。
つまり、変数が複数の CPU にキャッシュされている場合 (通常はマルチスレッド プログラミングで発生します)、キャッシュの不整合の問題が発生する可能性があります。
キャッシュの不整合の問題を解決するには、通常 2 つの解決策があります:
1) バス
# に LOCK# を追加する 2) キャッシュ整合性プロトコルを介して
# これら 2 つの方法はハードウェア レベルで提供されます。
初期の CPU では、キャッシュの不整合の問題は、バスに LOCK# ロックを追加することで解決されました。 CPU と他のコンポーネント間の通信はバスを介して行われるため、バスに LOCK# を追加すると、他の CPU が他のコンポーネント (メモリなど) にアクセスすることがブロックされ、1 つの CPU だけがこれを使用できるようになります。可変メモリ。たとえば、上記の例では、スレッドが i = i 1 を実行しており、このコードの実行中に LCOK# ロック信号がバス上に送信された場合、他の CPU はこのコードが完全に実行されるまで待つことしかできません。変数 i が配置されているメモリは変数を読み取り、対応する演算を実行します。これにより、キャッシュの不整合の問題が解決されます。
しかし、上記の方法には問題があり、バスをロックしている間は他のCPUがメモリにアクセスできず効率が悪くなります。
そこで登場したのがキャッシュコヒーレンスプロトコルです。最も有名なものは Intel の MESI プロトコルで、各キャッシュで使用される共有変数のコピーの一貫性が保証されます。その中心となるアイデアは、CPU がデータを書き込むときに、操作されている変数が共有変数であることが判明した場合、つまり、その変数のコピーが他の CPU にも存在する場合、他の CPU にその変数を設定するように通知する信号が送信されるというものです。したがって、他の CPU がこの変数を読み取る必要があり、この変数を自身のキャッシュにキャッシュするキャッシュ ラインが無効であることが判明すると、その変数をメモリから再読み取りします。
2. 同時プログラミングにおける 3 つの概念
並行プログラミングでは、通常、原子性の問題、可視性の問題、順序付けの問題という 3 つの問題に遭遇します。まず、これら 3 つの概念を詳しく見てみましょう:
1.原子性
原子性: つまり、1 つまたは複数の操作が完全に実行され、実行プロセスがいかなる要因によっても中断されないか、またはまったく実行されません。
非常に典型的な例は、銀行口座振替の問題です:
たとえば、口座 A から口座 B に 1,000 元を送金するには、口座 A から 1,000 元を減算し、口座 B に 1,000 元を追加するという 2 つの操作が必要です。
これら 2 つの操作がアトミックでない場合にどのような結果が生じるかを想像してみてください。口座 A から 1,000 元を引き落とした後、突然操作が終了されたとします。次に、B から 500 元を引き出し、500 元を引き出した後、アカウント B に 1,000 元を追加する操作を実行しました。この場合、アカウント A では 1,000 元が差し引かれますが、アカウント B では送金された 1,000 元を受け取ることができません。
したがって、予期しない問題が発生しないように、これら 2 つの操作はアトミックである必要があります。
同時プログラミングで同じ反映が行われた場合、どのような結果が生じるでしょうか?
最も単純な例として、32 ビット変数への代入プロセスがアトミックでない場合はどうなるかを考えてみましょう。
1i = 9;
スレッドがこのステートメントを実行する場合、32 ビット変数への値の代入には、下位 16 ビットへの値の代入と上位 16 ビットへの値の代入という 2 つのプロセスが含まれると一時的に仮定します。
その場合、下位 16 ビット値の書き込み時に突然中断され、この時点で別のスレッドが i の値を読み取り、間違ったデータが読み取られるという状況が発生する可能性があります。
2. 可視性
可視性とは、複数のスレッドが同じ変数にアクセスするときに、1 つのスレッドが変数の値を変更すると、他のスレッドが変更された値をすぐに確認できることを意味します。
簡単な例については、次のコードを見てください:
//コードはスレッド 1
によって実行されます int i = 0;
i = 10;
//コードはスレッド 2
によって実行されます j = i;
スレッド 1 が CPU1 によって実行される場合、スレッド 2 は CPU2 によって実行されます。上記の分析から、スレッド 1 が文 i = 10 を実行するとき、最初に i の初期値を CPU1 のキャッシュにロードし、次にそれを 10 に割り当て、次にキャッシュ内の i の値を割り当てることがわかります。 CPU1 の は 10 になりますが、すぐにはメインメモリに書き込まれません。
このとき、スレッド 2 は j = i を実行します。スレッド 2 はまずメイン メモリに移動して i の値を読み取り、CPU2 のキャッシュにロードします。この時点ではメモリ内の i の値はまだ 0 であることに注意してください。その場合、j の値は 0 になり、10 ではなくなります。
これは可視性の問題です。スレッド 1 が変数 i を変更した後、スレッド 2 はスレッド 1 によって変更された値をすぐには認識しません。
3. 秩序性
順序性: つまり、プログラムの実行順序はコードの順序で実行されます。簡単な例については、次のコードを見てください:
int i = 0;
ブール値フラグ = false;
i = 1; //ステートメント 1
flag = true; //ステートメント 2
上記のコードは、int型の変数とboolean型の変数を定義し、それぞれに値を代入しています。コード シーケンスから判断すると、ステートメント 1 はステートメント 2 の前にあります。では、JVM が実際にこのコードを実行するとき、ステートメント 1 がステートメント 2 より前に実行されることが保証されますか?必ずしもそうとは限りませんが、なぜですか?ここで命令の並べ替えが発生する可能性があります。
命令の並べ替えとは何かを説明します。一般に、プログラムの動作効率を向上させるために、プロセッサは入力コードを最適化することがあります。プログラム内の各ステートメントの実行順序がコード内の順序と一致していることを保証するものではありません。ただし、プログラムの最終的な実行結果がコードの順次実行の結果と一致していることを保証します。
たとえば、上記のコードでは、ステートメント 1 とステートメント 2 のどちらが先に実行されても、最終的なプログラムの結果には影響しませんが、実行中にステートメント 2 が最初に実行され、ステートメント 1 が後で実行される可能性があります。
ただし、プロセッサは命令の順序を変更しますが、プログラムの最終結果がコードの順次実行結果と同じになることを保証することに注意してください。次の例を見てください:
int a = 10; //ステートメント 1
int r = 2; //ステートメント 2
a = a 3; //ステートメント 3
r = a*a; //ステートメント 4
このコードには 4 つのステートメントがあるため、可能な実行順序は次のとおりです:
これが実行順序である可能性があります: ステートメント 2 ステートメント 1 ステートメント 4 ステートメント 3
プロセッサは並べ替え時に命令間のデータの依存関係を考慮するため、不可能です。命令 2 が命令 1 の結果を使用する必要がある場合、プロセッサは命令 1 が命令 2 より前に実行されるようにします。
並べ替えは単一スレッド内でのプログラムの実行結果には影響しませんが、マルチスレッドではどうなるでしょうか?例を見てみましょう:
//スレッド 1:
context =loadContext(); //ステートメント 1
inited = true; //ステートメント 2
//スレッド 2:
while(!開始されました){
寝る()###### }
doSomethingwithconfig(context);
上記のコードでは、ステートメント 1 とステートメント 2 にはデータの依存関係がないため、並べ替えることができます。並べ替えが発生した場合、スレッド 1 の実行中にステートメント 2 が最初に実行され、スレッド 2 は初期化作業が完了したと判断し、while ループから抜け出して doSomethingwithconfig(context) メソッドを実行しますが、今回はコンテキストが存在しないため、初期化するとプログラムエラーが発生します。
上記からわかるように、命令の並べ替えは単一スレッドの実行には影響しませんが、スレッドの同時実行の正確さには影響します。
言い換えれば、並行プログラムが正しく実行されるためには、原子性、可視性、および順序性が保証されなければなりません。いずれかが保証されていないと、プログラムが正しく動作しない可能性があります。
3. Java メモリ モデル
先ほど、メモリ モデルと同時プログラミングで発生する可能性のあるいくつかの問題について説明しました。 Java メモリ モデルを見て、Java メモリ モデルが提供する保証と、マルチスレッド プログラミングを実行する際のプログラム実行の正確性を保証するために Java で提供されるメソッドとメカニズムを検討してみましょう。
Java 仮想マシン仕様では、Java プログラムがさまざまなプラットフォームで一貫したメモリ アクセスを実現できるように、さまざまなハードウェア プラットフォームとオペレーティング システムの間のメモリ アクセスの違いを保護する Java メモリ モデル (JMM) を定義しようとしています。では、Java メモリ モデルは何を規定するのでしょうか? それは、プログラム内の変数のアクセス ルールを定義し、より広い範囲で、プログラムの実行順序を定義します。より良い実行パフォーマンスを得るために、Java メモリ モデルは、命令の実行速度を向上させるために実行エンジンがプロセッサのレジスタやキャッシュを使用することを制限したり、コンパイラが命令を並べ替えたりすることを制限しないことに注意してください。言い換えれば、Java メモリ モデルでは、キャッシュの一貫性の問題や命令の並べ替えの問題も発生します。
Java メモリ モデルでは、すべての変数がメイン メモリ (前述の物理メモリと同様) に格納され、各スレッドが独自の作業メモリ (前述のキャッシュと同様) を持つことが規定されています。スレッドによる変数の操作はすべて作業メモリ内で実行する必要があり、メインメモリ上で直接操作することはできません。また、各スレッドは他のスレッドの作業メモリにアクセスできません。
簡単な例を挙げると、Java では次のステートメントを実行します:
1i = 10;
実行スレッドは、まず変数 i が配置されているキャッシュ ラインを独自の作業スレッドに割り当ててから、それをメイン メモリに書き込む必要があります。値 10 をメイン メモリに直接書き込む代わりに。
では、Java 言語自体は、アトミック性、可視性、順序付けに関してどのような保証を提供しているのでしょうか?
1.原子性
Java では、基本データ型の変数への読み取りおよび割り当て操作はアトミック操作です。つまり、これらの操作は中断できず、実行されるか実行されないかのどちらかです。
上の文は簡単そうに見えますが、理解するのはそれほど簡単ではありません。次の例を見てください。
次のどの操作がアトミック操作であるかを分析してください:
x = 10; //ステートメント 1
y = x; //ステートメント 2
x; //ステートメント 3
x = x 1; //ステートメント 4
一見すると、上記の 4 つのステートメントの操作はすべてアトミック操作であると言う人もいるかもしれません。実際、ステートメント 1 だけがアトミック操作であり、他の 3 つのステートメントはアトミック操作ではありません。
ステートメント 1 は値 10 を x に直接割り当てます。これは、このステートメントを実行するスレッドが値 10 を作業メモリに直接書き込むことを意味します。
ステートメント 2 には、実際には 2 つの操作が含まれています。最初に x の値を読み取り、次に x の値を作業メモリに書き込みます。x の値の読み取りと作業メモリへの x の値の書き込みの 2 つの操作はアトミック操作ですが、 、しかしそれらは一緒になってアトミックな操作ではありません。
同様に、x と x = x 1 には、x の値の読み取り、1 の加算、新しい値の書き込みという 3 つの操作が含まれます。
したがって、上記の 4 つのステートメントのうち、ステートメント 1 の操作のみがアトミックです。
言い換えれば、単純な読み取りと代入 (そして数値は変数に代入する必要があり、変数間の相互代入はアトミック操作ではありません) のみがアトミック操作です。
ただし、ここで注意すべき点が 1 つあります。32 ビット プラットフォームでは、64 ビット データの読み取りと割り当てには 2 つの操作が必要であり、そのアトミック性は保証されません。しかし、最新の JDK では、JVM は 64 ビット データの読み取りと割り当てもアトミック操作であることを保証しているようです。
上記からわかるように、Java メモリ モデルでは、基本的な読み取りと代入がアトミックな操作であることのみが保証されていますが、より広範囲の操作でアトミック性を実現したい場合は、同期化とロックを使用することで実現できます。 synchronized と Lock では、常に 1 つのスレッドだけがコード ブロックを実行することが保証されるため、アトミック性の問題は発生せず、アトミック性が確保されます。
2. 可視性
可視性を確保するために、Java は可視性を確保するために volatile キーワードを提供します。
共有変数が volatile に変更されると、変更された値が直ちにメイン メモリに更新され、他のスレッドがその値を読み取る必要がある場合には、メモリから新しい値が読み取られます。
通常のシェア変数は、変更された後、いつメイン メモリに書き込まれるかが不確実であるため、可視性を保証できません。他のスレッドがそれを読み取る場合、この時点では元の古い値がまだメモリ内にある可能性があるため、視認性は保証されません。
さらに、synchronized と Lock によって可視性も保証できます。Synchronized と Lock を使用すると、1 つのスレッドだけが同時にロックを取得して同期コードを実行し、変数への変更は解放前にメイン メモリにフラッシュされます。ロック。したがって、視認性は保証されます。
3. 秩序性
Java メモリ モデルでは、コンパイラとプロセッサは命令を並べ替えることができますが、並べ替えプロセスはシングルスレッド プログラムの実行には影響しませんが、マルチスレッドの同時実行の正確さには影響します。
Java では、volatile キーワードを使用して、特定の「順序性」を確保できます (具体的な原理については次のセクションで説明します)。さらに、synchronized と Lock によって順序性を確保できます。明らかに、synchronized と Lock は、1 つのスレッドが常に同期コードを実行することを保証します。これは、スレッドに同期コードを順番に実行させるのと同じであり、自然に順序性が保証されます。
さらに、Java メモリ モデルには、生来の「順序性」、つまり、手段を選ばずに保証できる順序性があり、これはよく起こる前原則と呼ばれます。 2 つの操作の実行順序が前発生の原則から推定できない場合、それらの順序は保証されず、仮想マシンはそれらを自由に並べ替えることができます。
事前発生の原則を詳しく紹介しましょう:
プログラムシーケンスの規則: スレッド内では、コードの順序に従って、前に書かれた操作は後ろに書かれた操作より前に発生します。 ロック ルール: ロック解除操作が最初に発生してから、同じロックを使用した後続のロック操作が行われます。 揮発性変数のルール: 変数への書き込み操作は、その後の変数への読み取り操作の前に発生します。
配信ルール: 操作 A が操作 B の前に発生し、操作 B が操作 C の前に発生した場合、操作 A は操作 C
の前に発生すると結論付けることができます。 スレッド起動ルール: Thread オブジェクトの start() メソッドは、このスレッドのすべてのアクションで最初に発生します
スレッド中断ルール: スレッド中断() メソッドの呼び出しは、中断されたスレッドのコードが割り込みイベントの発生を検出したときに最初に発生します
スレッド終了ルール: スレッド内のすべての操作は、スレッドが終了されるときに最初に発生します。スレッドが終了したことは、Thread.join() メソッドと Thread.isAlive() の戻り値によって検出できます。 オブジェクトの終了ルール: オブジェクトの初期化は、finalize() メソッドの開始時に最初に行われます
これら 8 つの原則は、『Java 仮想マシンの徹底理解』から抜粋したものです。
これら 8 つのルールのうち、最初の 4 つのルールがより重要であり、最後の 4 つのルールは明らかです。
最初の 4 つのルールを説明しましょう:
プログラムの順序規則については、プログラム コードの一部が単一スレッド内で正常に実行されるように見える、というのが私の理解です。このルールでは、「前に書かれた操作は後ろに書かれた操作の前に発生する」と述べていますが、これは、仮想マシンが実行する可能性があるため、プログラムが実行されるように見える順序がコードの順序であることを意味する必要があることに注意してください。プログラムコードが変更され、命令が並べ替えられました。並べ替えは実行されますが、最終的な実行結果はプログラムの逐次実行結果と一致しており、データ依存関係のない命令のみが並べ替えられます。したがって、単一スレッドでは、プログラムの実行は順番に実行されるように見えますが、これを理解する必要があります。実際、このルールは単一スレッドでのプログラム実行結果の正確性を保証するために使用されますが、複数スレッドでのプログラム実行の正確性は保証できません。
2 番目のルールも理解しやすいもので、単一スレッドでも複数スレッドでも、同じロックがロックされている場合は、ロック操作を続行する前にまずロックを解放する必要があります。
3 番目のルールはより重要なルールであり、次の記事で焦点を当てます。直感的に説明すると、スレッドが最初に変数を書き込み、次にスレッドがそれを読み取る場合、読み取り操作の前に書き込み操作が確実に発生します。
4 番目のルールは、実際には、事前発生の原則の推移的な性質を反映しています。
4. 不安定なキーワードの詳細な分析
これまでにたくさんのことについて話しましたが、それらはすべて揮発性キーワードについて話すための道を開くものなので、次のトピックに入りましょう。
1.volatile キーワードの 2 つのレベルのセマンティクス
共有変数 (クラス メンバー変数、クラス静的メンバー変数) が volatile に変更されると、そのセマンティクスは 2 つのレベルになります:
1) さまざまなスレッドがこの変数を操作するときの可視性を確保します。つまり、1 つのスレッドが変数の値を変更すると、新しい値は他のスレッドからすぐに可視になります。
2) 命令の並べ替えは禁止されています。
まずコードを見てみましょう。スレッド 1 が最初に実行され、スレッド 2 が後で実行される場合:
//スレッド 1
ブール値停止 = false;
while(!stop){
doSomething();
}
//スレッド 2
stop = true;
このコードは非常に典型的なコードであり、多くの人がスレッドを中断するときにこのマーキング方法を使用する可能性があります。しかし実際、このコードは完全に正しく実行されるでしょうか?つまり、スレッドは中断されますか?必ずしもそうとは限りませんが、おそらくほとんどの場合、このコードはスレッドを中断する可能性がありますが、スレッドを中断できなくなる可能性もあります (この可能性は非常に低いですが、一度これが発生すると、無限ループが発生します)。
このコードによってスレッドが中断できなくなる理由を説明しましょう。前に説明したように、各スレッドは実行中に独自の作業メモリを持っているため、スレッド 1 の実行中に stop 変数の値をコピーし、それを独自の作業メモリに置きます。
次に、スレッド 2 が stop 変数の値を変更すると、それをメイン メモリに書き込む前に、スレッド 2 は他のことを実行します。その場合、スレッド 1 はスレッド 2 による stop 変数の変更を知りません。引き続きループします。下に進みます。
しかし、揮発性の変更を使用すると、状況は異なります:
1 つ目: volatile キーワードを使用すると、変更された値が直ちにメイン メモリに書き込まれます。
2 番目: volatile キーワードが使用されている場合、スレッド 2 が変更を行うと、スレッド 1 の作業メモリ内のキャッシュ変数 stop のキャッシュ ラインが無効になります (ハードウェア層に反映される場合、それは対応するキャッシュです) CPU の L1 または L2 キャッシュの行) 無効);
3 番目: スレッド 1 の作業メモリ内のキャッシュ変数 stop のキャッシュ ラインが無効であるため、スレッド 1 はメイン メモリに移動して変数 stop の値を再度読み取ります。
次に、スレッド 2 が stop 値を変更すると (もちろん、これには、スレッド 2 の作業メモリ内の値を変更し、変更された値をメモリに書き込むという 2 つの操作が含まれます)、変数 stop のキャッシュ ラインが次のようになります。無効な場合、スレッド 1 が読み取りを行うと、そのキャッシュ ラインが無効であることがわかります。キャッシュ ラインに対応するメイン メモリ アドレスが更新されるのを待ってから、最新の値を読み取ります。対応するメインメモリから。
次に、スレッド 1 が読み取るのは最新の正しい値です。
2. volatile はアトミック性を保証しますか?
上記のことから、volatile キーワードは操作の可視性を保証することがわかりましたが、volatile は変数の操作がアトミックであることを保証できますか?
例を見てみましょう:
パブリック クラス テスト {
public volatile int inc = 0;
public void増加() {
株式会社 ;
}
public static void main(String[] args) {
最終テスト test = new Test();
for(int i=0;i
新しいスレッド(){###### public void run() {
for(int j=0;j
test.increase();
};
}。始める();###### }
while(Thread.activeCount()>1) //前のスレッドがすべて実行されていることを確認します
Thread.yield();
System.out.println(test.inc);
}
}
考えてみてください、このプログラムの出力は何でしょうか?おそらく友人の中には10,000だと思っている人もいるでしょう。しかし、実際に実行してみると、結果は毎回一貫性がなく、常に 10,000 未満の数値であることがわかります。
疑問を持つ友人もいるかもしれませんが、いいえ、上記は変数 inc の自動インクリメント操作です。volatile は可視性を保証しているため、各スレッドで inc をインクリメントした後、他のスレッドでも見ることができます。変更された値は、10 スレッドで 1000 を実行しました。それぞれの演算を実行すると、inc の最終値は 1000*10=10000 になるはずです。
ここで誤解がありますが、volatile キーワードは可視性が正しいことを保証できますが、上記のプログラムのエラーは、アトミック性を保証できないことです。可視性は常に最新の値が読み取られることを保証することしかできませんが、揮発性は変数に対する操作のアトミック性を保証できません。
前述したように、自動インクリメント操作はアトミックではなく、変数の元の値の読み取り、1 の加算、作業メモリへの書き込みが含まれます。つまり、自動インクリメント操作の 3 つのサブ操作が個別に実行される可能性があり、次の状況が発生する可能性があります:
ある時点で変数 inc の値が 10 だった場合、
スレッド 1 は変数に対して自動インクリメント操作を実行します。スレッド 1 はまず変数 inc の元の値を読み取り、その後スレッド 1 はブロックされます。
次に、スレッド 2 は変数に対して自動インクリメント操作を実行し、スレッド 2 は変数 inc の元の値も読み取ります。スレッド 1 は変数 inc を読み取るだけで変数を変更しないため、スレッド 2 の作業は発生しません。メモリ内のキャッシュ変数 inc のキャッシュ ラインが無効であるため、スレッド 2 はメイン メモリに直接移動して inc の値を読み取ります。inc の値が 10 であることがわかり、1 を加算して 11 をスレッドに書き込みます。ワーキングメモリを作成し、最終的にメインメモリに書き込みます。
次に、スレッド 1 が 1 を加算します。inc の値が読み取られているため、この時点ではスレッド 1 の作業メモリ内の inc の値はまだ 10 であることに注意してください。そのため、スレッド 1 が inc に 1 を加算した後の inc の値は 11 になります。 . 、次に 11 を作業メモリに書き込み、最後にメインメモリに書き込みます。
次に、2 つのスレッドがそれぞれ自動インクリメント操作を実行した後、inc は 1 だけ増加しました。
このことを説明すると、友人の中には疑問を持つ人もいるかもしれませんが、変数が volatile 変数を変更すると、キャッシュ ラインが無効になることが保証されているのではありませんか?その後、他のスレッドが新しい値を読み取ります。はい、これは正しいです。これは、上記の事前発生ルールの揮発性変数ルールですが、スレッド 1 が変数を読み取ってブロックされた後は、inc 値が変更されないことに注意してください。この場合、volatile はスレッド 2 がメモリから変数 inc の値を確実に読み取ることができますが、スレッド 1 は変数 inc の値を変更しないため、スレッド 2 には変更された値がまったく表示されません。
根本的な原因はここにあり、自動インクリメント操作はアトミックな操作ではなく、volatile は変数に対する操作がアトミックであることを保証できません。
上記のコードを次のいずれかに変更すると、効果が得られます:
同期の使用:
パブリック クラス テスト {
public int inc = 0;
public synchronized void増加() {
株式会社 ;
}
public static void main(String[] args) {
最終テスト test = new Test();
for(int i=0;i
新しいスレッド(){###### public void run() {
for(int j=0;j
test.increase();
};
}。始める();###### }
while(Thread.activeCount()>1) //前のスレッドがすべて実行されていることを確認します
Thread.yield();
System.out.println(test.inc);
}
}
コードを表示
採用ロック:
パブリック クラス テスト {
public int inc = 0;
ロック lock = new ReentrantLock();
public void増加() {
lock.lock();
試す {###### 株式会社 ;
} ついに{###### 施錠開錠();###### }
}
public static void main(String[] args) {
最終テスト test = new Test();
for(int i=0;i
新しいスレッド(){###### public void run() {
for(int j=0;j
test.increase();
};
}。始める();###### }
while(Thread.activeCount()>1) //前の回線程都実行完了
を保護します Thread.yield();
System.out.println(test.inc);
}
}
コードを表示
採用AtomicInteger:
パブリック クラス テスト {
public AtomicInteger inc = new AtomicInteger();
public void増加() {
inc.getAndIncrement();
}
public static void main(String[] args) {
最終テスト test = new Test();
for(int i=0;i
新しいスレッド(){###### public void run() {
for(int j=0;j
test.increase();
};
}。始める();###### }
while(Thread.activeCount()>1) //前の回線程都実行完了
を保護します Thread.yield();
System.out.println(test.inc);
}
}
コードを表示
Java 1.5 の java.util.concurrent.atomic パケットでは、基本的なデータ型の独自(追加 1 オペレーション)、独自(减 1 オペレーション)、および追加法オペレーション(追加)などのいくつかのアトム オペレーション クラスが提供されています。アトミックは CAS を使用してアトム性操作を実行し (比較および交換)、CAS はプロセッサによって提供される CMPXCHG 命令を使用して実行されます。一方、プロセッサは CMPXCHG 命令を実行しますが、これはアトム性の操作です。 3.volatile能保证有順序性吗?
前述では、揮発性パスワードの禁止命令の順序について説明しました。したがって、揮発性はある程度の順序性を保証します。 volatile关键字禁止命令重排序有两层意思:
1) プログラムが揮発性の量の操作または書き込み操作を実行するとき、その前の操作の追加肯定はすべて実行されており、その結果は後の操作で確認できます。その後の操作はまだ実行されていません。
2) コマンド変換を行う場合、揮発性変数の後ろにある句を実行することはできません。また、揮発性変数の後ろにある句をその前に置くこともできません。 おそらく上記の比較、最も単純な例: //x、y は不揮発性の量 //フラグは揮発性です变量 x = 2; //语句1 y = 0; //语句2 フラグ = true; //语句3 x = 4; //语句4 y = -1; //语句5 フラグ量は揮発性の値であるため、命令順序を実行する際に、句3が先頭の句1、句2に配置されることはなく、また、句3がその後の句4、句5に配置されることもない。句 1 と句 2 の順序、句 4 と句 5 の順序は何の保証もありません。 また、揮発性キーワードは、句 3 まで実行する場合、句 1 と句 2 は実行完了している必要があり、句 1 と句 2 の実行結果は句 3、句 4、句 5 で確認可能であることを保証します。那么我们上の例に戻りました:
//回線程1:
コンテキスト = ロードコンテキスト(); //语句1
開始 = true; //语句2
//線程2:
while(!inited ){
寝る()###### }
doSomethingwithconfig(context);
前述の例では、セッション 2 がセッション 1 の前に実行される可能性が示唆されていますが、時間が経つとコンテキストも初期化されず、一方、プログラム 2 では初期化されていないコンテキストを使用して操作が実行され、プログラムに障害が発生する可能性があります。
# ここで、揮発性のキーワードを使用して開始値を変更すると、このような問題は発生しません。これは、句 2 に到達するときにコンテキストが完全に初期化されている必要があるためです。 4.揮発性の原理と実現機構# 前に volatile キーに由来するいくつかの使用法について説明しましたが、次に、可用性と禁止された命令の順序をどのように保護するかを検討します。 以下の部分の抜粋《深入理解Java仮想机》: 「揮発性ビットを追加した場合と、揮発性ビットを追加しなかった場合に生成された暗号コードの発行を確認してください。揮発性ビットを追加した場合、ロック前命令が多数出力される可能性があります。」 ロック前命令は 1 つの内部ストレージ (または内部ストレージ) に相当し、内部ストレージは 3 つの機能を提供します: 1)命令を並べ替えるときに、後の命令が内メモリの前の位置に追い出されたり、前の命令が内メモリの後ろに追い出されたりしないようにする。つまり、この命令が内メモリに実行されるとき、前の操作はすべて完了しました; 2)会議管理は、保存されている修正操作を直ちに書き込み主に保存します; 3)書き込み操作の場合、他の CPU のメモリに影響を与える可能性があります。 5.揮発性关键字の场景を使用する
synchronized キーワードは、複数のスレッドが 1 つのコードを同時に実行することを防ぎ、プログラムの実行効率に大きな影響を与えます。volatile キーワードは場合によっては synchronized よりもパフォーマンスが優れていますが、volatile キーワードはキーワードを置き換えることはできないことに注意してください。 volatile キーワードは操作のアトミック性を保証できないため、synchronized キーワード。一般に、volatile を使用するには、次の 2 つの条件を満たす必要があります。 1) 変数への書き込み操作は現在の値に依存しません
2) 変数は他の変数との不変条件に含まれていません
実際、これらの条件は、揮発性変数に書き込むことができる有効な値が、変数の現在の状態を含むプログラムの状態から独立していることを示します。
実際、私の理解では、上記の 2 つの条件は、 volatile キーワードを使用するプログラムが同時実行中に正しく実行できるように、操作がアトミック操作であることを保証する必要があるということです。
Java で volatile が使用されるシナリオをいくつか紹介します。
1.ステータスマーク金額
揮発性ブールフラグ = false;
while(!flag){
doSomething();
}
public void setFlag() {
フラグ = true;
}
volatile boolean inited = false;
//スレッド 1:
context =loadContext();
開始 = true;
//スレッド 2:
while(!開始されました){
寝る()###### }
doSomethingwithconfig(context);
2.再確認
# クラスシングルトン{ プライベート揮発性静的シングルトン インスタンス = null; プライベートシングルトン() { } パブリック静的シングルトン getInstance() { if(インスタンス==null) { 同期済み (Singleton.class) { if(インスタンス==null) インスタンス = new Singleton(); } } インスタンスを返す; } }以上がJava で volatile キーワードを使用する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。