揮発性の特性
共有変数を揮発性として宣言すると、この変数の読み取り/書き込みは非常に特殊になります。volatileの特性を理解する良い方法は、volatile変数の単一の読み取り/書き込みを、同じモニターロックを使用してこれらの単一の読み取り/書き込み操作を同期するものとして扱うことです。特定の例を使用して説明しましょう。次のサンプルコードを参照してください。
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用 volatile 声明 64 位的 long 型变量
public void set(long l) {
vl = l; // 单个 volatile 变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile 变量的读 / 写
}
public long get() {
return vl; // 单个 volatile 变量的读
}
}
上記のプログラムの3つのメソッドを呼び出す複数のスレッドがあるとします。このプログラムは、次のプログラムと意味的に同等です。
class VolatileFeaturesExample {
long vl = 0L; // 64 位的 long 型普通变量
public synchronized void set(long l) { // 对单个的普通 变量的写用同一个监视器同步
vl = l;
}
public void getAndIncrement () { // 普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() {
// 对单个的普通变量的读用同一个监视器同步
return vl;
}
}
上記のサンプルプログラムに示すように、揮発性変数の単一の読み取り/書き込み操作は、同じモニターロックを使用する通常の変数の読み取り/書き込み操作と同期され、それらの間の実行効果は同じです。
モニターロックの発生前ルールは、モニターを解放してモニターを取得する2つのスレッド間のメモリの可視性を保証します。つまり、揮発性変数の読み取りは、この揮発性変数の(任意のスレッド)最後の書き込みを常に確認できます。
モニターロックのセマンティクスは、クリティカルセクションコードの実行がアトミックであることを決定します。つまり、64ビットのlong変数とdouble変数の場合でも、揮発性変数である限り、変数の読み取りと書き込みはアトミックになります。volatile ++に類似した複数の揮発性操作または複合操作がある場合、これらの操作は全体としてアトミックではありません。
つまり、揮発性変数自体には次の特性があります。
- 可視性。揮発性変数を読み取ると、この揮発性変数への最後の書き込みを常に(任意のスレッドで)確認できます。
- アトミック性:単一の揮発性変数の読み取り/書き込みはアトミックですが、volatile ++と同様の複合操作はアトミックではありません。
揮発性の書き込み-読み取りは、関係が確立される前に発生します
上記は揮発性変数の特性に関するものです。プログラマーにとって、揮発性がスレッドメモリの可視性に与える影響は、揮発性自体の特性よりも重要であり、さらに注意を払う必要があります。
JSR-133以降、揮発性変数の読み書きにより、スレッド間の通信を実現できます。
メモリセマンティクスの観点から、volatileはモニターロックと同じ効果があります。volatile書き込みとモニターリリースは同じメモリセマンティクスを持ち、volatile読み取りとモニター取得は同じメモリセマンティクスを持ちます。
揮発性変数を使用した次のサンプルコードを参照してください。
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}
スレッドAがwriter()メソッドを実行した後、スレッドBがreader()メソッドを実行するとします。発生前のルールによると、このプロセスで確立された関係が発生する前に発生することは、2つのカテゴリに分類できます。
- プログラムシーケンスルールによれば、1は2の前に発生し、3は4の前に発生します。
- 揮発性ルールによると、2は3の前に発生します。
- 前に発生するという推移的なルールによれば、1は4の前に発生します。
上記のグラフィック表現は、関係が次のようになる前に発生します。
上の図では、各矢印でリンクされた2つのノードは、関係の前に発生することを表しています。黒い矢印はプログラムシーケンスルールを示し、オレンジ色の矢印は揮発性ルールを示し、青い矢印はこれらのルールを組み合わせることによって提供される保証前に発生することを示します。
ここで、スレッドAが揮発性変数を書き込んだ後、スレッドBは同じ揮発性変数を読み取ります。揮発性変数を書き込む前にスレッドAに表示されるすべての共有変数は、スレッドBが同じ揮発性変数を読み取った直後にスレッドBに表示されます。
揮発性の書き込み/読み取りメモリのセマンティクス
volatileによって記述されたメモリセマンティクスは次のとおりです。
- 揮発性変数を書き込むとき、JMMはスレッドに対応するローカルメモリ内の共有変数をメインメモリにフラッシュします。
上記のサンプルプログラムVolatileExampleを例にとると、スレッドAが最初にwriter()メソッドを実行し、次にスレッドBがreader()メソッドを実行するとします。最初は、2つのスレッドのローカルメモリ内のフラグとaは両方とも初期状態です。 。次の図は、スレッドAが揮発性書き込みを実行した後の共有変数の状態の概略図です。
上の図に示すように、スレッドAがフラグ変数を書き込んだ後、スレッドAによって更新されたローカルメモリAの2つの共有変数の値がメインメモリにフラッシュされます。このとき、ローカルメモリAとメインメモリの共有変数の値は同じです。
揮発性読み取りのメモリセマンティクスは次のとおりです。
- 揮発性変数を読み取るとき、JMMはスレッドに対応するローカルメモリを無効にします。次に、スレッドはメインメモリから共有変数を読み取ります。
以下は、スレッドBが同じ揮発性変数を読み取った後の共有変数の状態の概略図です。
上図に示すように、フラグ変数を読み取った後、ローカルメモリBが無効になっています。この時点で、スレッドBはメインメモリから共有変数を読み取る必要があります。スレッドBの読み取り操作により、ローカルメモリBとメインメモリの共有変数の値が一致します。
揮発性書き込みと揮発性読み取りの2つのステップを一緒に見ると、スレッドBを読み取った後、揮発性変数を読み取った後、この揮発性変数を書き込む前の書き込みスレッドAのすべての表示されている共有変数値はすぐに読み取りスレッドBに表示されます。
以下は、揮発性書き込みと揮発性読み取りのメモリセマンティクスの要約です。
- スレッドAは揮発性変数を書き込みます。本質的に、スレッドAはメッセージ(共有変数を変更する)をスレッドに送信し、次に揮発性変数を読み取ります。
- スレッドBは揮発性変数を読み取ります。本質的に、スレッドBは、以前にスレッドから送信されたメッセージを受信します(この揮発性変数を書き込む前の共有変数の変更)。
- スレッドAは揮発性変数を書き込み、次にスレッドBは揮発性変数を読み取ります。このプロセスは基本的にスレッドAがメインメモリを介してスレッドBにメッセージを送信することです。
揮発性メモリセマンティクスの実装
次に、JMMが揮発性の書き込み/読み取りのメモリセマンティクスを実装する方法を見てみましょう。
並べ替えは、コンパイラの並べ替えとプロセッサの並べ替えに分けられることを前述しました。揮発性メモリのセマンティクスを実装するために、JMMはこれら2つのタイプの並べ替えタイプをそれぞれ制限します。以下は、JMMがコンパイラー用に作成した揮発性の並べ替えルールの表です。
再注文できます | 2番目の操作 | ||
最初の操作 | 通常の読み取り/書き込み | 揮発性読み取り | 揮発性書き込み |
通常の読み取り/書き込み | 番号 | ||
揮発性読み取り | 番号 | 番号 | 番号 |
揮発性書き込み | 番号 | 番号 |
たとえば、3行目の最後のセルは次のことを意味します。プログラムシーケンスで、最初の操作が共通変数の読み取りまたは書き込みである場合、2番目の操作が揮発性書き込みである場合、コンパイラはこの2つの操作を並べ替えることができません。
上記の表から、次のことがわかります。
- 2番目の操作が揮発性書き込みの場合、最初の操作が何であっても、並べ替えることはできません。このルールにより、揮発性書き込みの前の操作が、コンパイラーによって揮発性書き込みの後に並べ替えられないことが保証されます。
- 最初の操作が揮発性読み取りである場合、2番目の操作が何であっても、並べ替えることはできません。このルールにより、揮発性読み取り後の操作が、揮発性読み取りの前にコンパイラーによって並べ替えられないことが保証されます。
- 最初の操作が揮発性書き込みで、2番目の操作が揮発性読み取りの場合、並べ替えは実行できません。
volatileのメモリセマンティクスを実現するために、コンパイラは命令シーケンスにメモリバリアを挿入して、バイトコードを生成するときに特定のタイプのプロセッサの並べ替えを禁止します。コンパイラーにとっては、挿入バリアの総数を最小限に抑えるための最適な配置を見つけることはほとんど不可能であるため、JMMは保守的な戦略を採用しています。以下は、保守的な戦略に基づくJMMメモリバリア挿入戦略です。
- 各揮発性書き込み操作の前にStoreStoreバリアを挿入します。
- 各揮発性書き込み操作の後にStoreLoadバリアを挿入します。
- 各揮発性読み取り操作の後にLoadLoadバリアを挿入します。
- 各揮発性読み取り操作の後にLoadStoreバリアを挿入します。
上記のメモリバリア挿入戦略は非常に保守的ですが、任意のプロセッサプラットフォームおよび任意のプログラムで正しい揮発性メモリセマンティクスを確実に取得できます。
以下は、保守的な戦略の下で揮発性書き込みがメモリバリアに挿入された後に生成される命令シーケンスの概略図です。
上の図のStoreStoreバリアは、揮発性書き込みの前にすべてのプロセッサに表示される前のすべての通常の書き込み操作を保証できます。これは、StoreStoreバリアにより、上記のすべての通常の書き込みが、揮発性の書き込みの前にメインメモリにフラッシュされることが保証されるためです。
ここでさらに興味深いのは、揮発性書き込みの背後にあるStoreLoadバリアです。このバリアの機能は、揮発性書き込みの並べ替えと、それに続く揮発性読み取り/書き込み操作の可能性を回避することです。コンパイラーは、揮発性書き込みの後にStoreLoadバリアを挿入する必要があるかどうかを正確に判断できないことが多いためです(たとえば、メソッドは揮発性書き込みの直後に戻ります)。揮発性メモリのセマンティクスを正しく実装するために、JMMはここで保守的な戦略を採用しています。つまり、各揮発性書き込みの後、または各揮発性読み取りの前にStoreLoadバリアを挿入します。全体的な実行効率の観点から、JMMは各揮発性書き込みの後にStoreLoadバリアを挿入することを選択しました。揮発性の書き込み/読み取りメモリセマンティクスの一般的な使用パターンは次のとおりです。1つの書き込みスレッドが揮発性変数を書き込み、複数のリーダースレッドが同じ揮発性変数を読み取ります。リーダースレッドの数がライタースレッドの数を大幅に超える場合、揮発性書き込みの後にStoreLoadバリアを挿入することを選択すると、実行効率が大幅に向上します。ここから、JMMの実装における特徴を見ることができます。最初に正確性を確保し、次に実行効率を追求します。
以下は、保守的な戦略の下で揮発性読み取りがメモリバリアに挿入された後に生成される命令シーケンスの概略図です。
上の図のLoadLoadバリアは、プロセッサが上記の揮発性読み取りと以下の通常の読み取りを並べ替えることを禁止するために使用されます。LoadStoreバリアは、プロセッサが上記の揮発性読み取りと以下の通常の書き込みを並べ替えることを禁止するために使用されます。
揮発性書き込みおよび揮発性読み取りに対する上記のメモリバリア挿入戦略は非常に保守的です。実際の実行では、揮発性の書き込み/読み取りのメモリセマンティクスが変更されない限り、コンパイラは特定の状況に応じて不要なバリアを省略できます。以下に、特定のサンプルコードを示します。
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一个 volatile 读
int j = v2; // 第二个 volatile 读
a = i + j; // 普通写
v1 = i + 1; // 第一个 volatile 写
v2 = j * 2; // 第二个 volatile 写
}
...... // 其他方法
}
readAndWrite()メソッドの場合、コンパイラーはバイトコードを生成するときに次の最適化を行うことができます。
最後のStoreLoadバリアは省略できないことに注意してください。2番目のvolatileが書き込まれた後、メソッドはすぐに戻ります。現時点では、コンパイラーは、後で揮発性の読み取りまたは書き込みが行われるかどうかを正確に判断できない場合があります。安全上の理由から、コンパイラーはここにStoreLoadバリアを挿入することがよくあります。
上記の最適化は、どのプロセッサプラットフォームにも当てはまります。プロセッサが異なれば「タイト」なプロセッサメモリモデルも異なるため、特定のプロセッサメモリモデルに応じてメモリバリアの挿入を最適化することもできます。x86プロセッサを例にとると、上の図の最後のStoreLoadバリアを除いて、すべてのバリアが省略されます。
以前の保守的な戦略の下での揮発性の読み取りと書き込みは、x86プロセッサプラットフォームで次のように最適化できます。
前述のように、x86プロセッサは書き込み/読み取り操作のみを並べ替えます。X86は、読み取り、読み取り、書き込み、および書き込み/書き込み操作を並べ替えないため、これら3つのタイプの操作に対応するメモリバリアはx86プロセッサでは省略されます。x86では、JMMは、揮発性の書き込みと読み取りのメモリセマンティクスを正しく実装するために、揮発性の書き込みの後にStoreLoadバリアを挿入するだけで済みます。これは、x86プロセッサでは、揮発性書き込みのオーバーヘッドが揮発性読み取りのオーバーヘッドよりもはるかに大きいことを意味します(StoreLoadバリアのオーバーヘッドが比較的大きいため)。
JSR-133が揮発性のメモリセマンティクスを強化する必要がある理由
JSR-133より前の古いJavaメモリモデルでは、揮発性変数間の並べ替えは許可されていませんでしたが、古いJavaメモリモデルでは、揮発性変数と通常の変数間の並べ替えが許可されていました。古いメモリモデルでは、VolatileExampleサンプルプログラムを次の順序で実行するように並べ替えることができます。
古いメモリモデルでは、1と2の間にデータの依存関係がない場合、1と2を並べ替えることができます(3と4は類似しています)。結果は次のとおりです。リーダースレッドBが4を実行すると、ライタースレッドAが1を実行したときに共有変数の変更が表示されるとは限りません。
したがって、古いメモリモデルでは、揮発性の書き込み(モニターを解放せずに読み取る)は、それが持つメモリセマンティクスを取得します。JSR-133エキスパートグループは、モニターロックよりもスレッド間の通信メカニズムを軽量化するために、volatileのメモリセマンティクスを強化することを決定しました。コンパイラとプロセッサによる揮発性変数と通常の変数の並べ替えを厳密に制限して、揮発性の書き込みを保証します。読み取りと監視のリリース取得のメモリセマンティクスは同じです。コンパイラの並べ替えルールとプロセッサのメモリバリア挿入戦略の観点から、揮発性変数と通常の変数の間の並べ替えがvolatileのメモリセマンティクスを破壊する可能性がある限り、この並べ替えはコンパイラの並べ替えルールとプロセッサのメモリバリアによって並べ替えられます。挿入戦略は禁止されています。
volatileは、単一のvolatile変数の読み取り/書き込みのアトミック性のみを保証するため、モニターロックの相互に排他的な実行特性により、クリティカルセクションコード全体の実行のアトミック性を保証できます。機能の点では、モニターロックはvolatileよりも強力です。スケーラビリティと実行パフォーマンスの点では、volatileにはより多くの利点があります。読者がプログラムでモニターロックの代わりに揮発性を使用したい場合は、注意してください。
前の記事 Javaメモリモデルの詳細な理解(3)-逐次一貫性 |
次の記事 Javaメモリモデルの詳細な理解(5)-ロック |
この記事に貢献してくれた作者に感謝します
Cheng Xiaoming、Javaソフトウェアエンジニア、全国認定システムアナリスト、情報プロジェクトマネージャー。並行プログラミングに焦点を当て、富士通南大で働きます。個人の電子メール:[email protected]。
---------------------
作成者:ワールドコーディング
ソース:CSDN
オリジナル:https://blog.csdn.net/dgxin_605/article/details/86182774
著作権:This記事はブロガーのオリジナル記事ですので、転載する場合はブログ投稿へのリンクを添付してください!