ディレクトリナビゲーション
序文
このセクションでは、Javaでのマルチスレッドの原則について深く理解しています。このトピックには5つのサブセクションがあります。
- 並行プログラミングの基礎
- 並行プログラミングの実現原理
- 並行プログラミングの原則(上)
- 並行プログラミングの原理(下記)
- 同時戦闘
このセクションのハイライト:
-
Javaメモリモデル(Javaメモリモデル、JMM)
-
JMMが原子性、可視性、秩序の問題をどのように解決するか
-
同期された揮発性
Javaメモリモデル
メモリモデルは、共有メモリシステム内のマルチスレッドプログラムの読み取りおよび書き込み操作動作の仕様を定義して、さまざまなハードウェアおよびオペレーティングシステムのメモリアクセスの違いを保護し、Javaプログラムがすべてのプラットフォームで一貫したメモリアクセス効果を実現できるようにします。Javaメモリモデルの主な目標は、プログラム内の各変数のアクセスルールを定義することです。つまり、仮想マシンに変数を格納し、メモリから変数を削除します(ここでの変数は共有変数、つまりインスタンスオブジェクトを指します。静的フィールド、配列オブジェクト、およびヒープメモリに格納されているその他の変数。ローカル変数の場合、これらはスレッド専用であり、共有されません)。これらのルールは、メモリへの読み取りおよび書き込み操作を調整して、命令実行の正確さを保証するために使用されます。
これは、プロセッサー、キャッシュ、並行性、およびコンパイラーに関連しています。彼は、CPUマルチレベルキャッシュ、プロセッサの最適化、命令の再配置などによって引き起こされるメモリアクセスの問題を解決し、並行シナリオでの可視性、原子性、および順序を保証しました。メモリモデルは、主に同時実行の問題を解決するために2つの方法を使用します。プロセッサの最適化を制限する方法とメモリバリアを使用する方法です。
Javaメモリモデルは、スレッドとメモリ間の相互作用を定義します。JMM抽象モデルでは、メインメモリとワーキングメモリに分割されます。メインメモリはすべてのスレッドで共有され、ワーキングメモリは各スレッドに固有です。スレッドによる変数のすべての操作(読み取り、割り当て)は、作業メモリーで実行する必要があり、メインメモリー内の変数を直接読み書きすることはできません。さらに、異なるスレッドは互いの作業メモリ内の変数にアクセスできません。スレッド間の変数値の転送は、メインメモリを介して完了する必要があります。3つのスレッド間の相互作用は次のとおりです。
したがって、一般的にJMMは仕様です。目的は、複数のスレッドが共有メモリを介して通信し、コンパイラがコード命令を並べ替え、プロセッサがコードを順不同で実行する場合のローカルメモリデータの不整合を解決することです。 。目的は、並行プログラミングシナリオで原子性、可視性、および順序を確保することです。
推奨される記事は次のとおりです。Javaメモリモデルとは何ですか。
拡張読み取り:JVMメモリモデル(JMM)とガベージコレクター(GC)の詳細な理解
JMMが原子性、可視性、秩序の問題をどのように解決するか
実際、スレッドセーフの問題は次のように要約できます:可視性、原子性、秩序、これらの問題、私たちはこれらの問題を理解し、それらを解決する方法を知っています、そしてマルチスレッドセーフの問題は問題ではありません
Java 1.8ソースコードHotSpotでは、CPUキャッシュとメインメモリ間の共有変数のアトミック性、可視性、および順序の問題を解決するのと同様の手がかりを見ることができます。Javaの世界では、JMM(Javaメモリモデル)モデルマルチスレッドの安全性などの問題を維持するために使用されます。
同時処理に関連する一連のキーワード(volatile、Synchronized、final、jucなど)がJavaで提供されます。これらは、Javaメモリモデルが基盤となる実装をカプセル化した後に開発者に提供されるキーワードです。マルチスレッドコードの開発では、時間の経過とともに、同期などのキーワードを直接使用して並行性を制御できるため、基礎となるコンパイラの最適化やキャッシュの一貫性の問題を気にする必要がありません。したがって、Javaメモリモデルでは、一連の仕様を定義するだけでなく、また、オープン命令は最下層にカプセル化され、開発者に提供されます。
- 原子保証
2つの高度なバイトコード命令monitorとmonitorexitがjavaで提供されます。Javaで同期されることは、コードブロック内の操作がアトミックであることを保証するために対応しています。
- 視認性保証
Javaのvolatileキーワードは機能を提供します。つまり、変更された変数は変更された直後にメインメモリに同期でき、変更された変数は使用される前に毎回メインメモリから更新されます。したがって、volatileを使用して、マルチスレッド操作中に変数の可視性を確保できます。
揮発性に加えて、Javaで同期された2つのキーワードとfinalも可視性を実現できます。
- 秩序ある保証
Javaでは、同期および揮発性を使用して、複数のスレッド間の操作の秩序を確保できます。実装方法は異なります。
volatileキーワードは、命令の再配置を禁止します。
同期キーワードは、同時に1つのスレッド操作のみが許可されることを保証します。
揮発性が可視性を保証する方法
hsdisツールをダウンロードします。https: //sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download
解凍後、jreディレクトリのサーバーパスに保存されます
次に、main関数を実行します。main関数を実行する前に、次の仮想マシンパラメーターを追加します。
-server -Xcomp -XX:+ UnlockDiagnosticVMOptions -XX:+ PrintAssembly-XX:CompileCommand = compileonly、* App.getInstance(実際の実行コードに置き換えます)
揮発性変数によって変更された共有変数には、操作の書き込み時にロックプレフィックス付きの追加のアセンブリ命令があります。この命令は、CPUキャッシュについて説明したときに言及されました。これにより、キャッシュの一貫性によってバスロックまたはキャッシュロックがトリガーされます。可視性を解決するための合意問題。
volatileとして宣言された変数への書き込み操作の場合、JVMはLockプレフィックス命令をプロセッサに送信して、変数が配置されているキャッシュラインのデータをシステムメモリに書き込み、前述のMESIキャッシュの整合性に従います。複数のCPUで各キャッシュのデータの一貫性を確保するための性的合意。
揮発性は命令の並べ替えを防ぎます
命令の再配置の目的は、CPUの使用率とパフォーマンスを最大化することです。CPUのアウトオブオーダー実行の最適化は、シングルコア時代の正確性には影響しませんが、マルチコア時代では、マルチスレッドはさまざまな分野で真の並列処理を実現できます。コア。、スレッド間でデータが共有されると、予期しない問題が発生する可能性があります。
命令の並べ替えが従わなければならない原則は、コード実行の最終結果に影響を与えないということです。コンパイラとプロセッサは、データ依存関係を持つ2つの操作の実行順序を変更しません(ここで説明するデータ依存関係は、1つの操作のみを対象としています)。プロセッサで実行される命令とシングルスレッドで実行される操作。)
このセマンティクスは、実際にはシリアルとしてのセマンティクスです。並べ替えがどのように行われたとしても、シングルスレッドプログラムの実行結果は変更されず、コンパイラとプロセッサはシリアルとしてのセマンティクスに準拠する必要があります。
マルチコアおよびマルチスレッドでの命令再配置の影響
public class ThreadTest {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("x=" + x + "->y=" + y);
}
}
コンパイラの並べ替えとキャッシュの可視性が考慮されていない場合、上記のコードの可能な結果はx = 0、y = 1; x = 1、y = 0; x = 1、y = 1これらの3つの結果です。t1/ t2順次実行することも、逆に実行することも、t1 / t2を交互に実行することもできますが、このコードの実行結果はx = 0、y = 0の場合もあります。これは、スレッドt1内の2行のコード間にデータ依存関係がないため、アウトオブオーダー実行の結果です。したがって、x = bは、a = 1の前にアウトオブオーダーになる可能性があり、同時にスレッド内にあります。 t2 y = aは、t1のa = 1よりも前に実行することもできるため、実行順序は次のようになります。
t1:x = b
t2:b = 1
t2:y = a
t1:a = 1
したがって、上記の例から、並べ替えは可視性の問題を引き起こします。ただし、DCLの部分的な初期化など、すべての命令が単純な読み取りまたは書き込みであるとは限らないため、並べ替えによって引き起こされる問題の深刻さは可視性よりもはるかに大きくなります。したがって、可視性の問題を単に解決するだけでは不十分であり、プロセッサの並べ替えの問題を解決する必要があります。
メモリバリア
メモリバリアは、前述の2つの問題を解決する必要があります。1つはコンパイラの最適化障害とCPUの実行障害です。最適化バリアとメモリバリアの2つのメカニズムを使用して、それらを解決できます。
CPUレベルからメモリバリアとは何かを理解する
CPUのアウトオブオーダー実行の性質は、マルチCPUマシンでは、各CPUにキャッシュがあることです。特定のデータが特定のCPUに初めて取得されるとき、そのデータはCPUキャッシュ、それはメモリから取得でき、CPUキャッシュにロードされた後、キャッシュからすばやくアクセスできます。CPUが書き込み操作を実行するときは、他のCPUがデータを安全に変更できるように、他のCPUがキャッシュからデータを削除したことを確認する必要があります。明らかに、複数のキャッシュがある場合、データの不整合の問題を回避するためにキャッシュ整合性プロトコルを使用する必要があります。この通信プロセスにより、アクセスの順序が狂う問題、つまり実行時のメモリへのアクセスの順序が狂う可能性があります。 。
現在のCPUアーキテクチャはメモリバリア機能を提供します。x86CPUでは、対応するメモリバリアが実装されています。書き込みバリア、負荷バリア、および完全バリアが実装されています。主な機能は次のとおりです。
-
命令間の並べ替えを防ぐ
-
データの可視性を確保する
- ストアバリア
ストアバリアは書き込みバリアと呼ばれ、ストアストアバリアと同等です。ストアストアメモリバリアの前のすべての実行をメモリバリアの前に強制的に実行し、キャッシュ無効信号を送信します。ストアストアバリア命令の後のすべてのストア命令は、ストアストアバリアが実行される前の命令の後に実行する必要があります。つまり、書き込みバリアの前後の命令が並べ替えられます。はい、ストアバリアの前に発生したすべてのメモリ更新が表示されます(ここに表示されると、変更された値が表示され、操作結果が表示されます)
- 負荷バリア
負荷バリアは読み取りバリアと呼ばれ、負荷バリアに相当します。これにより、負荷バリアの後のすべての負荷命令が負荷バリアの後に実行されます。つまり、バイナリシステムは、ロードバリアの読み取りバリアの前後でロード命令を並べ替え、ストアバリアと連携して、ストアバリアの前に発生するすべてのメモリ更新が、ロードバリア後のロード操作から見えるようにします。
- 完全な障壁
フルバリアはフルバリアになります。これはストアロードに相当し、前の2つのバリアの効果があるため、オールラウンドバリアです。ストアロードバリアの前のすべてのストア/ロード命令はバリアの前に強制的に実行され、バリアの後のすべてのストア/ロード命令はバリアの後に実行されます。ストアロードバリアの前後で命令を並べ替えることは禁止されています。
概要:メモリバリアは、シーケンシャル整合性の問題のみを解決し、キャッシュ整合性の問題は解決しません。キャッシュ整合性は、CPUのキャッシュロックとMESIプロトコルによって実現されます。キャッシュ整合性プロトコルは、順次整合性ではなく、キャッシュ整合性のみを考慮します。だからこれらは2つの質問です
コンパイラレベルでの命令の並べ替えの問題を解決する方法
コンパイラレベルでは、volatileキーワードは、コンパイラレベルでのキャッシュと並べ替えをキャンセルします。プログラムをコンパイルするときの最適化バリアの前の命令が、最適化バリアの後に実行されないことを確認してください。これにより、コンパイル中の最適化が実際のコードロジックシーケンスに影響を与えないことが保証されます。
ハードウェアアーキテクチャ自体がメモリの可視性を保証している場合、volatileは空のタグであり、セマンティックメモリバリアを挿入しません。ハードウェアアーキテクチャ自体がプロセッサの並べ替えを実行せず、より強力な並べ替えセマンティクスがある場合、volatileは空のタグであり、関連するセマンティクスでメモリバリアを挿入しません。
JMMでは、メモリバリア命令は4つのカテゴリに分類され、特定のタイプのプロセッサは、メモリの可視性を確保するために、さまざまなセマンティクスの下でさまざまなメモリバリアを使用して並べ替えられます。
LoadLoadバリア、load1; LoadLoad; load2、load1データのロードがload2および後続のすべてのロード命令のロードよりも優先されるようにします
StoreStoreバリア、store1; storestore; store2、store2および後続のすべてのストレージ命令の前にstore1データが他のプロセッサに表示されるようにします
LoadStore Barries、load1; loadstore; store2、load1データのロードがstore2よりも優先され、後続のストレージ命令がメモリにフラッシュされるようにします
StoreLoad Barries、store1; storeload; load2、load2および後続のすべてのロード命令をロードする前に、store1データが他のプロセッサに表示されるようにします。このメモリバリア命令は、CPUレベルのメモリの前にある汎用バリアです。バリアで言及されました。他の3つの障壁の効果もあります
揮発性が原子性を保証できない理由
public class Demo {
volatile int i;
public void incr(){
i++;
}
public static void main(String[] args) {
new Demo().incr();
}
}
実行後、javap -cDemo.classを使用してバイトコードを表示します
アトミックインクリメント操作の場合、次の3つのステップがあります。
- 揮発性変数の値をローカルに読み取ります。
- 変数の値を増やします。
- ローカル値を書き戻して、他のスレッドから見えるようにします。
最後に書く
このセクションのデモンストレーションコードアドレス:
https://github.com/harrypottry/ThreadDemo
アーキテクチャに関する知識の詳細については、この一連の記事に注意してください:Javaアーキテクトの成長の道