可視性の問題からの考え
コードの一部を見てみましょう:
public static boolean flg =false;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while (!flg){
i++;
//1. System.out.println("i:="+i);
// 2.Thread.sleep(1000);
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
});
thread.start();
Thread.sleep(1000);
flg=true;
}
操作結果:
最初のコメントコードまたは2番目のコメントコードを離すと、プログラムが正常に終了することがわかります。なぜですか。分析してみましょう
印刷するとループが終了します
-
印刷の最下層はsynchronizedキーワードを使用しているため、printlnにロック操作があり、ロックを解除する操作により、作業メモリーに関連する書き込み操作がメインメモリーに強制的に同期されることを示します。
public void println(String x) { synchronized (this) { print(x); newLine(); } }
-
IOの観点からは、印刷は本質的にIO操作であり、ディスクのIO効率はCPUの計算効率よりもはるかに遅くなければならないため、IOはCPUにメモリを更新する時間を与え、この現象を引き起こします。新しいFile()を定義することで確認できます
Thread.sleep(long)
- Thread.sleep(long)はスレッドの切り替えを引き起こし、キャッシュの無効化を引き起こしてから、最新の値を読み取ります
揮発性
記事の冒頭で述べた問題が発生する主な理由は、可視性であることがわかっています。スレッドの可視性を確保するために、javaはvolatileキーワードを提案しました。
可視性とは何ですか?
マルチスレッド環境では、スレッドが共有変数を更新した後、その後変数にアクセスするスレッドは、更新された結果をすぐに読み取れないか、更新された結果を読み取れない場合があります。これは、スレッドの安全性の問題のもう1つの兆候です。可視性です。
なぜ可視性の問題があるのですか?
1.キャッシュ
最新のプロセッサ(CPU)の処理能力は、メインメモリ(DRAM)のアクセスレートよりもはるかに優れています。メインメモリが読み取りおよび書き込み操作を実行する時間は、プロセッサが数百の命令を実行するのに十分です。プロセッサとメインメモリとの間のギャップを埋めるために、ハードウェア設計者は、導入されたキャッシュ、メインメモリとプロセッサとの間を、図に示すように、:
キャッシュは、メインメモリよりもアクセス速度はるかに大きいです。メインメモリよりも容量がはるかに小さいストレージコンポーネントであり、各プロセッサには独自のキャッシュがあります。キャッシュの導入後、プロセッサは読み取りおよび書き込み操作を実行するときにメインメモリを直接処理するのではなく、キャッシュを介して処理します。
上の図に示すように、最近のプロセッサには通常、複数のレベルのキャッシュがあります。レベル1キャッシュ(L1キャッシュ)、レベル2キャッシュ(L2キャッシュ)、およびレベル3キャッシュ(L3キャッシュ)があります。アクセスの順序:L1> L2> L3。
複数のスレッドが同じ共有変数にアクセスすると、各スレッドのプロセッサのキャッシュが共有変数のコピーを保持します。これにより、プロセッサがコピーデータを更新するときに問題が発生します。 、他のプロセッサはどのようにして適切に認識し、反応しますか?これには、可視性の問題が含まれます。キャッシュコヒーレンシ問題とも呼ばれます
キャッシュ整合性の問題(MESI)
MESI(Modified-Exclusive-Shared-Invalid)プロトコルは、広く使用されているキャッシュコヒーレンシプロトコルです。x86プロセッサで使用されるコヒーレンシプロトコルは、MESIプロトコルに基づいています。
データの一貫性を確保するために、MESIはキャッシュエントリのステータスを4つのタイプ(変更済み、排他的、共有、無効)に分割し、さまざまなプロセッサ間の読み取りおよび書き込み操作を調整する一連のメッセージ(メッセージ)を定義します。
- 無効(無効、Iとしてマーク)は、対応するキャッシュ行にメモリアドレスに対応する有効なコピーが含まれていないことを意味します。この状態は、キャッシュエントリの初期状態です。
- 共有(共有、Sで示される)は、図に示すように、対応するキャッシュラインに対応するメモリアドレスに対応するコピーデータが含まれ、他のプロセッサのキャッシュにも同じメモリアドレスに対応するコピーデータが含まれることを意味します。
- 排他的(排他的、Eで示される)は、図に示すように、対応するキャッシュ行に対応するメモリアドレスに対応するコピーデータが含まれ、他のすべてのプロセッサのキャッシュがコピーデータを保持しないことを意味します。
- 変更済み(変更済み、Mとしてマーク)。これは、対応するキャッシュ行に、対応するメモリアドレスの更新された結果データが含まれていることを意味します。MESIプロトコルでは、一度に1つのプロセッサのみが同じメモリアドレスに対応するデータを更新できます。図:
MESIプロトコルは、さまざまなプロセッサの調整、メモリ書き込み操作を読み取るための一連のメッセージ(メッセージ)を次のように定義します。
次に、ワークプロセスのフローチャートを簡単に見ていきます。MESIプロトコル:
上記からMESIプロトコルのパフォーマンスの弱点を確認できます。プロセッサがメモリ操作を実行した後、他のすべてのプロセッサが対応するコピーデータをキャッシュにキャッシュし、これらのプロセッサから
無効化確認/読み取り応答を受信するのを待つ必要があります。メッセージの後でのみ、データをキャッシュに書き込むことができます。
このような待機によって引き起こされる書き込み操作の遅延を回避および削減するために、ハードウェア設計者は書き込みキャッシュと無効化キューを導入しました。
バッファの書き込み(バッファの保存)とキューの無効化(キューの無効化)
書き込みバッファ(ストアバッファ、書き込みバッファとも呼ばれます)は、プロセッサ内のプライベートキャッシュコンポーネントであり、容量はキャッシュよりも小さくなります。書き込みバッファが導入された後、プロセッサは次のように操作を処理します。対応するキャッシュエントリがSの場合、プロセッサは最初に書き込み操作の関連データ(操作付きのデータとメモリアドレスを含む)を書き込みバッファに格納します。エントリで、Invalidateメッセージを非同期に送信します。つまり、メモリ書き込み操作の実行プロセッサは、書き込み操作の関連データを書き込みバッファに入れた後、書き込み操作が完了したと見なし、他のプロセッサがInvalidate Acknowledge / Readを返すのを待ちません。応答メッセージは他の命令を実行し続けるため、書き込み操作の遅延が減少します。
キューを無効にします。無効メッセージを受信した後、プロセッサはメッセージで指定されたメモリアドレスに対応するコピーデータを削除せず、メッセージを送信します。無効化キューに格納された後、Invalidate Acknowledgeメッセージが返されるため、書き込み操作実行者の待機時間が短縮されます。
プロセスは次のとおりです。
ただし、書き込みバッファと無効化キューにより、いくつかの新しい問題が発生します。命令の並べ替え。
2.命令の並べ替え
例を使用して、命令の並べ替えの問題を詳細に説明します。
int data =0;
boolean ready=false;
void threadDemo1(){
data=1; //S1
ready=true; //S2
}
void threadDemo2(){
while (!ready){
//S3
System.out.println(data); //S4
}
}
CPU0のキャッシュにreadyのコピーのみがあり、CPU1のキャッシュにデータのコピーしかない場合、
実行プロセスは次のよう
になります。CPU1の観点から、これにより、S1の前にS2が実行されるという現象が発生します。
メモリバリア
プロセッサはどのような種類のメモリの並べ替えをサポートしており、対応する並べ替えを禁止できる命令を提供します。これらの命令はメモリバリアと呼ばれます。
メモリバリアはXYで表すことができ、XとYのサブテーブルはロード(読み取り)とストアを表します。 (書く)。メモリバリアの機能は、命令の左側のX操作と命令の右側のY操作の間の並べ替えを禁止し、命令の左側のすべてのX操作が命令の右側のY操作の前に送信されるようにすることです。以下に示すように:
原理
volatileの原理は、基礎となるメモリバリアを使用することで実際に実現されます。volatileキーワードを追加した後、疑似コードを確認できます。
volatile int data =0;
boolean ready=false;
void threadDemo1(){
data=1;
//StoreStore 确保前后的写操作已经写入到高速缓存中
ready=true;
}
void threadDemo2(){
while (!ready){
//LoadLoad 确保ready的读操作在data的读操作之前
System.out.println(data);
}
}
要約すると、揮発性の読み取り/書き込み挿入メモリバリアルール:
- 各揮発性読み取り操作の後にLoadLoadバリアとLoadStoreバリアを挿入します
- 各揮発性書き込み操作の前後にStoreStoreバリアとStoreLoadバリアを挿入します
起こる-モデルの前
Javaメモリモデル(JMM)は、volatile、final、およびsynchronizedキーワードの動作を定義し、適切に同期されたJavaプログラムが異なるアーキテクチャのプロセッサで実行できるようにします。
アトミック性の観点から、JMMは、long / doubleおよび参照タイプの共有変数以外の基本データタイプの読み取りおよび書き込み操作はすべてアトミックであると規定しています。さらに、JMMは、揮発性で変更されたlong / double共有変数に対する読み取りおよび書き込み操作もアトミックであることも具体的に規定しています。
可視性と順序の問題について、JMMは次のように発生前モデルを使用して発生
前ルールに回答します。
- プログラムシーケンスルール:一見シリアルセマンティクス(As-if-serial)。スレッド内のアクションの結果は、プログラムシーケンス内のアクションの後に他のアクションに表示され、これらのアクションはスレッド自体に実行され、プログラムシーケンス内で完全に送信されたように見えます。
- モニターロックルール:モニターロックの解放は、後続の各ロック申請の前に行われます。
注:「リリース」と「アプリケーション」は、同じタイプのロックインスタンス用である必要があります。つまり、別のロックのアプリケーションとの関係の前に、1つのロックのリリースは発生しません。 - 揮発性変数のルール:揮発性変数の書き込み操作が発生します-変数の後続の各読み取り操作の前に。
注:同じ揮発性変数に対するものである必要があります。次に、同じ揮発性変数に対する読み取りおよび書き込み操作には、時系列の関係が必要です。 - スレッド開始ルール:スレッドのstart()メソッドを呼び出します-開始されるスレッド内のアクションの前に発生します。
- スレッド終了ルール:スレッド内のすべてのアクションが発生します-joinメソッドが戻った後にスレッドのjoinメソッドによって実行されるアクションの前
- 遷移規則:AがBの前に発生し、BがCの前に発生した場合、AはCの前に発生します。
総括する
- Volatileは、キャッシュの一貫性を通じて可視性を実現します
- Volatileは、メモリバリアを介した命令の並べ替えを禁止し、それによって秩序を確保します
- JMMは、発生前モデルを使用して、可視性と順序の問題をより簡潔に記述します。