記事ディレクトリ
1 はじめに
メモリ バリアは、メモリ フェンス、メモリ バリア、またはメモリ フェンス命令とも呼ばれ、マルチスレッドにおける命令の並べ替えの問題を解決するために使用される同期プリミティブです。
最新のマルチコア プロセッサでは、プロセッサとコンパイラが命令を再配置して処理効率を向上させます。通常、この最適化はシングルスレッド環境では問題ありませんが、マルチスレッド環境では問題が発生する可能性があります。2 つのスレッドが共有変数にアクセスすると、命令の並べ替えによりエラーやデータの不整合が発生する可能性があるためです。
現時点では、この問題を解決するにはメモリバリアが必要です。メモリ バリアは、メモリ バリアを実行する前にプロセッサにすべての読み取り/書き込み操作を強制的に完了させ、メモリ バリア後のすべての読み取り/書き込み操作は、メモリ バリア前の操作が完了した後でのみ開始されるようにします。言い換えれば、メモリバリアは命令の並べ替えを防ぎ、命令がプログラムと同じ順序で実行されることを保証します。
メモリ バリアの機能は、メモリ バリアの前の操作がメモリ バリアの後にキューに入れられないこと、およびメモリ バリアの後の操作がメモリ バリアの前のキューに入れられないことを保証することにより、データの順序性を確保し、命令の再配置によって引き起こされるエラーを回避することです。
2. JVM メモリ モデル (JMM) の概要
JVM のメモリ モデル、つまり Java メモリ モデル (JMM) は、主に、同時プロセスにおけるアトミック性、可視性、順序付けの 3 つの特性を処理する方法を中心に構築されたモデルです。
Java では、すべてのインスタンス フィールド、静的フィールド、および配列要素はヒープ メモリに格納され、複数のスレッド間で共有されます。ローカル変数、メソッド定義パラメータ、および例外ハンドラ パラメータはスレッド間で共有されず、データ競合の影響を受けません。
これらの共有データに対するスレッド操作は、メイン メモリを通じて調整する必要があります。スレッドは相互に直接通信できず、スレッドによる共有データの読み取りと書き込みはすべてメイン メモリを経由する必要があります。ただし、効率を向上させるために、メイン メモリ データを読み取るとき、各スレッドはまず変数の値を自身の作業メモリ (CPU のキャッシュに相当) にコピーし、次にこれらのコピーに対して操作を行ってから、新しい値が書き込まれます。メインメモリに戻ります。
2 つのスレッドが同時に変数を操作する場合、一方のスレッドは変数の値を変更しますが、新しい値がメイン メモリに書き戻される前に、もう一方のスレッドが変数の値を読み取ります。その時点で読み取られるのは古い値です。つまり、あるスレッドによる共有変数の変更は別のスレッドにはすぐに表示されない可能性があり、これには可視性の問題が伴います。
命令の並べ替えにより、スレッドに一貫性のない変数値が表示される場合もあります。命令の実行効率を向上させるために、CPU およびコンパイラは命令を再配置する場合があります。適切な同期手段がなければ、特にマルチスレッド環境では、命令の並べ替えによって予期しない結果が生じる可能性があります。
したがって、可視性と順序付けの問題を解決するために、Java メモリ モデルにはメモリ バリアなどのメカニズムが導入され、同時実行時のデータの一貫性と順序付けが保証されます。
III. ホットスポットのメモリバリア
いくつかの質問について考えてみましょう
-
- ホットスポットはどこにメモリバリアを挿入しますか?
-
- 4 種類のメモリ バリア: LoadLoad、StoreStore、LoadStore、StoreLoad とそれぞれの仕組み
-
- ホットスポットがさまざまなタイプのメモリバリアを使用して、JMM で前発生関係を実装する方法
Java の実装として、HotSpot 仮想マシンはメモリ バリア テクノロジを使用して、Java メモリ モデル (JMM) の可視性と秩序性を確保します。メモリ バリアは、命令の並べ替えを防止し、データの可視性を確保するために使用される CPU 命令の特別なセットです。HotSpot は、重要な場所にメモリ バリアを挿入して、JMM で前発生関係を実装します。
メモリ バリアには 4 つのタイプがあります。
-
LoadLoad バリア: バリアの後ろで読み取り操作を開始する前に、LoadLoad バリアの前のすべての読み取り操作が完了していることを確認します。
-
StoreStore バリア: バリアの後ろの書き込み操作が開始される前に、StoreStore バリアの前のすべての書き込み操作が完了していることを確認します。
-
LoadStore バリア: バリアの後ろの書き込み操作が開始される前に、LoadStore バリアの前のすべての読み取り操作が完了していることを確認します。
-
StoreLoad バリア: StoreLoad バリアの前のすべての書き込み操作が、バリアの後ろの読み取り操作が開始される前に完了していることを確認します。
JMM で前発生関係を実装するには、次のようにさまざまなタイプのメモリ バリアが使用されます。
-
volatile 変数の読み取り操作の場合、volatile 変数を読み取る前に他のスレッドがこの変数への書き込みを完了していることを確認するために、読み取り操作の前に LoadLoad バリアを挿入する必要があります。同時に、読み取り操作の後に LoadStore バリアが挿入され、後続の書き込み操作の前に揮発性変数の読み取りが確実に完了します。
-
volatile 変数への書き込み操作の場合、書き込み操作の前に StoreStore バリアを挿入して、volatile 変数に書き込む前に他のスレッドがこの変数への書き込みを完了していることを確認する必要があります。同時に、書き込み操作の後に StoreLoad バリアが挿入され、後続の読み取り操作の前に揮発性変数の書き込みが確実に完了します。
-
ロックの解放操作では、ロックが解放される前に共有データの書き込みが確実に完了するように、ロックが解放される前に StoreStore バリアを挿入する必要があります。このようにして、別のスレッドがロックを取得すると、前のスレッドによって共有データに加えられた変更を確認できます。
-
ロック取得操作の場合、ロックを取得する前に共有データの読み取りが確実に完了するように、ロックの取得後に LoadLoad バリアを挿入する必要があります。このようにして、現在のスレッドがロックを取得した後、前のスレッドによる共有データの変更を確認できるようになります。
HotSpot 仮想マシンは、主要な場所にさまざまなタイプのメモリ バリアを挿入することで、マルチスレッド環境でスレッド間のデータ アクセスに特定の順序と可視性を持たせることができ、それによって JMM での発生前関係を実現します。
4. 例: ホットスポットで揮発性変数がメモリバリアをどのように使用するか
- 揮発性変数の書き込み操作は、他のスレッドの読み取り操作よりも前に発生します。
- ホットスポットは、揮発性変数の読み取り操作と書き込み操作の間に StoreLoad バリアを挿入します。
volatile
Java メモリ モデルでは変数には特別なセマンティクスがあり、変数の書き込み操作は他のスレッドによる読み取り操作よりも前に発生します。これは、1 つのスレッドがvolatile
変数に書き込むと、その変数を読み取る後続のスレッドがそのvolatile
書き込みを認識することを意味します。
volatile
ホットスポット仮想マシンでは、変数のセマンティクスを保証するために、volatile
変数の書き込み操作の後、読み取り操作の前に StoreLoad バリアが挿入されます。
変数に書き込む場合volatile
、書き込み操作が後続の書き込み操作で並べ替えられるのを防ぎ、書き込み操作がすべてのプロセッサーに確実に認識されるように、書き込み操作の後に StoreStore バリアが挿入されます。次に、StoreLoad バリアを挿入して、書き込み操作とその後の読み取り操作の順序変更を防ぎます。
変数を読み取る場合volatile
、読み取り操作とその後の読み取り操作の順序変更を防ぎ、最新のデータを確実に読み取れるようにするために、読み取り操作の後に LoadLoad バリアが挿入されます。同時に、読み取り操作とその後の書き込み操作の順序変更を防ぐために、LoadStore バリアが挿入されます。これにより、volatile
変数の読み取り後のすべての操作でvolatile
変数の最新の値が確認されるようになります。
変数の読み取り操作と書き込み操作の間に StoreLoad バリアを挿入することにより、ホットスポット仮想マシンは変数の書き込み操作が他のスレッドの読み取り操作より前に発生することをvolatile
保証し、それによって変数のセマンティクスを実現します。スレッド A とスレッド B という 2 つのスレッドがあるとします。スレッド A は変数の更新を担当し、スレッド B はこの変数の読み取りを担当します。volatile
volatile
volatile
v
関連するコードは次のとおりです。
スレッド A:
v = 1; // 写操作
...
スレッド B:
int a = v; // 读操作
...
ホットスポット仮想マシンでは、揮発性セマンティクスを保証するために、上記のコードが実際に実行されるときに、次の形式でメモリ バリアが挿入されます。
スレッド A:
v = 1; // 写操作
StoreStore屏障
StoreLoad屏障
...
スレッド B:
LoadLoad屏障
LoadStore屏障
int a = v; // 读操作
...
スレッド A では、v = 1;
後で StoreStore バリアと StoreLoad バリアが挿入されます。StoreStore バリアは、後続の書き込み操作の進行を防止するために使用され、StoreLoad バリアは、後続の読み取り操作の進行を防止するために使用されます。
スレッド B では、int a = v;
LoadLoad バリアと LoadStore バリアが読み取り操作の前に挿入されます。LoadLoad バリアは、後続の読み取り操作が進められるのを防ぐために使用され、LoadStore バリアは後続の書き込み操作が進められるのを防ぐために使用されます。
v
このようにして、ホットスポット仮想マシンは、揮発性変数の書き込み操作が他のスレッドの読み取り操作よりも前に行われるようにします。つまり、スレッド B は、スレッドAによる変数の更新を確認できます。
5。結論
- Hotspot はメモリ バリアを使用することで、コンパイラとプロセッサが前発生関係を尊重しながら、重要なメモリ操作の誤った命令の並べ替えを実行しないようにします。
- これにより、プログラマーはマルチスレッド環境で正確で信頼性の高いコードを作成できます。
6. 参考資料
- oracle Java JMM 公式ドキュメント: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4