ホームページ  >  記事  >  Java  >  Javaメモリモデルの詳細な分析: 基本部分

Javaメモリモデルの詳細な分析: 基本部分

黄舟
黄舟オリジナル
2016-12-29 13:02:281419ブラウズ

同時プログラミング モデルの分類

同時プログラミングでは、スレッド間で通信する方法とスレッド間で同期する方法という 2 つの重要な問題に対処する必要があります (ここでのスレッドとは、同時に実行するアクティブなエンティティを指します)。通信とは、スレッドが情報を交換するメカニズムを指します。命令型プログラミングでは、スレッド間に共有メモリとメッセージ パッシングという 2 つの通信メカニズムがあります。

共有メモリ同時実行モデルでは、プログラムの共通状態がスレッド間で共有され、スレッドはメモリ内の共通状態の書き込みと読み取りによって暗黙的に通信します。メッセージ パッシング同時実行モデルでは、スレッド間にパブリックな状態は存在せず、スレッドはメッセージを明示的に送信することによって明示的に通信する必要があります。

同期とは、異なるスレッド間で操作が行われる相対的な順序を制御するためにプログラムによって使用されるメカニズムを指します。共有メモリ同時実行モデルでは、同期は明示的に実行されます。プログラマは、メソッドまたはコード部分がスレッド間で排他的に実行される必要があることを明示的に指定する必要があります。メッセージ パッシング同時実行モデルでは、メッセージの送信はメッセージの受信よりも前に行われる必要があるため、同期は暗黙的に実行されます。

Java の同時実行性は共有メモリ モデルを採用しており、Java スレッド間の通信は常に暗黙的に実行され、通信プロセス全体がプログラマにとって完全に透過的です。マルチスレッド プログラムを作成する Java プログラマが、暗黙的なスレッド間通信がどのように機能するかを理解していないと、あらゆる種類の奇妙なメモリ可視性の問題に遭遇する可能性があります。

Java メモリ モデルの抽象化

Java では、すべてのインスタンス フィールド、静的フィールド、配列要素はヒープ メモリに格納され、ヒープ メモリはスレッド間で共有されます (この記事では、インスタンス フィールドを指すために「共有変数」という用語を使用します。静的フィールドと配列要素)。ローカル変数、メソッド定義パラメータ (Java 言語仕様では形式メソッド パラメータと呼ばれます)、および例外ハンドラ パラメータはスレッド間で共有されず、メモリの可視性の問題はなく、メモリ モデルの影響を受けません。

Java スレッド間の通信は Java メモリ モデル (この記事では JMM と呼びます) によって制御され、あるスレッドによる共有変数への書き込みが別のスレッドにいつ表示されるかを決定します。抽象的な観点から、JMM はスレッドとメイン メモリ間の抽象的な関係を定義します。スレッド間の共有変数はメイン メモリ (メイン メモリ) に格納され、各スレッドは共有メモリのコピーであるプライベート ローカル メモリ (ローカル メモリ) を持ちます。スレッドが読み書きする変数はローカル メモリに保存されます。ローカル メモリは JMM の抽象概念であり、実際には存在しません。キャッシュ、書き込みバッファ、レジスタ、その他のハードウェアとコンパイラの最適化について説明します。 Java メモリ モデルの抽象的な概略図は次のとおりです。

Javaメモリモデルの詳細な分析: 基本部分

上の図から、スレッド A とスレッド B が通信したい場合、次の 2 つの手順を実行する必要があります。
まず、スレッド A がローカル メモリを更新します。 A 渡されたシェア変数はメインメモリに更新されます。
その後、スレッド B はメイン メモリに移動し、スレッド A が以前に更新した共有変数を読み取ります。

以下は、これら 2 つのステップを説明するための概略図です:

Javaメモリモデルの詳細な分析: 基本部分

上の図に示すように、ローカル メモリ A と B は、メイン メモリに共有変数 x のコピーを持っています。最初、これら 3 つのメモリの x 値がすべて 0 であると仮定します。スレッド A の実行中、スレッド A は更新された x 値 (値が 1 であると仮定) を独自のローカル メモリ A に一時的に保存します。スレッド A とスレッド B が通信する必要がある場合、スレッド A はまずローカル メモリ内の変更された x 値をメイン メモリにリフレッシュします。このとき、メイン メモリ内の x 値は 1 になります。続いて、スレッド B はメイン メモリに行き、スレッド A の更新された x 値を読み取ります。このとき、スレッド B のローカル メモリの x 値も 1 になります。

全体として見ると、これら 2 つのステップは本質的にスレッド A がスレッド B にメッセージを送信することであり、この通信プロセスはメイン メモリを経由する必要があります。 JMM は、メイン メモリと各スレッドのローカル メモリ間の相互作用を制御することにより、Java プログラマにメモリの可視性を保証します。

並べ替え

プログラム実行時のパフォーマンスを向上させるために、コンパイラーとプロセッサーは命令の順序を変更することがよくあります。並べ替えには 3 つのタイプがあります:
コンパイラ最適化並べ替え。コンパイラは、シングルスレッド プログラムのセマンティクスを変更せずに、ステートメントの実行順序を再配置できます。
命令レベルの並列並べ替え。最新のプロセッサは、命令レベル並列処理 (ILP) を使用して、複数の命令を重複して実行します。データの依存関係がない場合、プロセッサはステートメントが機械語命令に対応する順序を変更できます。
メモリシステムの並べ替え。プロセッサはキャッシュと読み取り/書き込みバッファを使用するため、ロードおよびストア操作が順序どおりに実行されていないように見える可能性があります。

Java ソース コードから実際に実行される最終命令シーケンスまで、次の 3 つの並べ替えが行われます。

Javaメモリモデルの詳細な分析: 基本部分

上記の 1 はコンパイラの並べ替えに属し、2 と 3 はプロセッサの並べ替えに属します。このような並べ替えにより、マルチスレッド プログラムでメモリの可視性の問題が発生する可能性があります。コンパイラの場合、JMM のコンパイラの並べ替え規則は、特定の種類のコンパイラの並べ替えを禁止しています (すべてのコンパイラの並べ替えが禁止されているわけではありません)。プロセッサの並べ替えについては、JMM のプロセッサ並べ替えルールにより、Java コンパイラは命令シーケンスを生成するときに特定の種類のメモリ バリア (インテルではメモリ フェンスと呼んでいます) 命令を挿入し、メモリ バリア命令を使用して特定の種類のプロセッサの並べ替えを禁止する必要があります (すべてのプロセッサではありません)。並べ替えは無効にする必要があります)。

JMM は、異なるコンパイラや異なるプロセッサ プラットフォーム上で特定の種類のコンパイラの並べ替えとプロセッサの並べ替えを禁止することにより、プログラマに一貫したメモリの可視性を保証する言語レベルのメモリ モデルです。

プロセッサの並べ替えとメモリバリア命令

最新のプロセッサは書き込みバッファを使用して、メモリに書き込まれたデータを一時的に保存します。書き込みバッファは命令パイプラインの実行を維持し、データがメモリに書き込まれるのを待っている間にプロセッサが停止することによって引き起こされる遅延を回避します。同時に、バッチプロセスで書き込みバッファをリフレッシュし、書き込みバッファ内の同じメモリアドレスへの複数の書き込みをマージすることにより、メモリバスの使用量を削減できます。書き込みバッファには非常に多くの利点がありますが、各プロセッサ上の書き込みバッファは、それが配置されているプロセッサにのみ表示されます。この機能は、メモリ操作の実行順序に重要な影響を与えます。プロセッサがメモリ操作を読み書きする順序は、メモリが実際に読み書き操作を行う順序と必ずしも一致しません。具体的に説明するには、次の例を見てください。
プロセッサ A
プロセッサ B
a = 1; //A2
b = 2; // B2
初期状態: a = b = 0

プロセッサは実行を許可し、結果を取得します: x = y = 0
プロセッサ A とプロセッサ B がプログラムの順序でメモリアクセスを並行して実行すると仮定すると、最終的にはx = y = 0 の結果。具体的な理由は以下の図に示されています:



ここで、プロセッサ A とプロセッサ B は、同時に共有変数を自身の書き込みバッファ (A1、B1) に書き込み、次に別の共有変数 (A2、B2) をメモリから読み取り、最後に自分自身をバッファ領域に書き込むことができます。に保存されているデータはメモリ (A3、B3) にフラッシュされます。このタイミングでプログラムを実行すると、x = y = 0 という結果が得られます。

実際のメモリ操作のシーケンスから判断すると、プロセッサ A が自身の書き込みキャッシュをリフレッシュするために A3 を実行するまで、書き込み操作 A1 は実際には実行されません。プロセッサ A はメモリ操作を A1 -> A2 の順序で実行しますが、メモリ操作が実際に行われる順序は A2 -> A1 です。このとき、プロセッサ A のメモリ操作順序が並べ替えられます (プロセッサ B の状況はプロセッサ A と同じなので、ここでは詳しく説明しません)。

ここで重要なのは、書き込みバッファはそれ自身のプロセッサーのみに見えるため、プロセッサーがメモリ操作を実行する順序が実際のメモリ操作の実行順序と一致しないということです。最新のプロセッサは書き込みバッファを使用するため、書き込みと読み取りの操作を並べ替えることができます。

一般的なプロセッサで許可されている並べ替えタイプのリストは次のとおりです:
Load-Load
Load-Store
Store-Store
Store-Load
データ依存関係
sparc-TSO
N
N
N
Y
N
x86
N
N
N
Y
N
ia64
Y
Y
Y
Y
N
PowerPC
Y
Y
Y
Y
N
上記の表のセル内の「N」は、プロセッサが2 つのオペレーションの並べ替えは許可されません。「Y」は並べ替えが許可されていることを意味します。

上記の表からわかるように、共通プロセッサではストアロードの並べ替えが可能ですが、共通プロセッサではデータに依存する操作の並べ替えは許可されていません。 sparc-TSO と x86 は、書き込み/読み取り操作の並べ替えのみを許可する比較的強力なプロセッサ メモリ モデルを備えています (どちらも書き込みバッファを使用するため)。

※注 1: sparc-TSO は、TSO (Total Store Order) メモリ モデルで実行する場合の sparc プロセッサの特性を指します。

※注2:上記表のx86にはx64とAMD64が含まれます。

※注3: ARMプロセッサのメモリモデルはPowerPCプロセッサのメモリモデルと非常に似ているため、この記事では無視します。

※注4:データの依存関係については後ほど具体的に説明します。

メモリの可視性を確保するために、Java コンパイラは、生成された命令シーケンス内の適切な位置にメモリ バリア命令を挿入し、特定の種類のプロセッサの並べ替えを禁止します。 JMM は、メモリ バリア命令を次の 4 つのカテゴリに分類します:
バリア タイプ
命令の例
説明
ロード ロード バリア
Load1; Load2
Load2 の前に Load1 データをロードし、後続のすべてのロード命令をロードします。
StoreStore バリア
Store1; StoreStore;Store2
Store2 へのストアおよび後続のすべてのストア命令の前に、Store1 データが他のプロセッサに表示される (メモリにフラッシュされる) ことを確認します。
LoadStore バリア
Load1; Store2
Store2 より前に Load1 データがロードされ、後続のすべてのストア命令がメモリにフラッシュされるようにします。
StoreLoad バリア
Store1;StoreLoad2
Store1 のデータが、Load2 および後続のすべてのロード命令によってロードされる前に、他のプロセッサーに表示されるようにします (メモリへのフラッシュを指します)。 StoreLoad バリアにより、バリア前のすべてのメモリ アクセス命令 (ストアおよびロード命令) が、バリア後のメモリ アクセス命令が実行される前に完了します。
StoreLoad バリアは、他の 3 つのバリアの効果を同時に持つ「万能」バリアです。最新のマルチプロセッサのほとんどは、このバリアをサポートしています (他のタイプのバリアは、すべてのプロセッサでサポートされているわけではありません)。現在のプロセッサは通常、書き込みバッファ内のすべてのデータをメモリにフラッシュする必要があるため (バッファ完全フラッシュ)、このバリアの実行はコストがかかる可能性があります。

以前の出来事

JDK5 以降、Java は新しい JSR-133 メモリ モデルを使用します (特に明記されていない限り、この記事では JSR-133 メモリ モデルに焦点を当てます)。 JSR-133 は、事前発生の概念を提案しており、これを通じて操作間のメモリの可視性が説明されています。 1 つの操作の結果を別の操作から参照できるようにする必要がある場合、2 つの操作間に前発生関係が存在する必要があります。ここで説明した 2 つの操作は、1 つのスレッド内で行うことも、異なるスレッド間で行うこともできます。 プログラマーと密接に関係する前発生ルールは次のとおりです:
プログラム シーケンス ルール: スレッド内の各操作は、そのスレッド内の後続の操作よりも前に発生します。
モニター ロック ルール: モニター ロックのロック解除は、その後のモニター ロックのロックの前に行われます。
Volatile 変数のルール: volatile フィールドへの書き込みは、この volatile フィールドのその後の読み取りより前に行われます。
推移性: A が B より前に発生し、B が C より前に発生した場合、A は C より前に発生します。

2 つの操作の間には前発生の関係があることに注意してください。これは、前の操作を後の操作の前に実行する必要があるという意味ではありません。 happens-before は、前の操作 (実行の結果) が次の操作から認識可能であること、および最初の操作が 2 番目の操作から認識可能であり、2 番目の操作より前に順序付けされていることのみを必要とします。前発生の定義は非常に微妙です。次の記事では、前発生がこのように定義されている理由を具体的に説明します。

happens-before と JMM の関係を以下の図に示します。

Javaメモリモデルの詳細な分析: 基本部分

上の図に示すように、happens-before ルールは通常、複数のコンパイラの並べ替えルールとプロセッサの並べ替えルールに対応します。 Java プログラマにとって、happens before ルールはシンプルで理解しやすいものです。これにより、プログラマは、JMM によって提供されるメモリの可視性の保証を理解するために、複雑な並べ替えルールとこれらのルールの具体的な実装を学習する必要がなくなります。

上記は Java メモリ モデルの詳細な分析です: 基本部分 さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。