データの依存関係
2 つの操作が同じ変数にアクセスし、2 つの操作のうち 1 つが書き込み操作である場合、2 つの操作の間にデータの依存関係があります。データの依存関係は以下の 3 種類に分類されます:
名前
コード例
説明
書き込み後に読み取ります
a = 1;
変数を書き込んだ後、この位置を再度読み取ります。
書いてから書く
a = 1;a = 2;
変数を書いた後、この変数を再度書きます。
読んでから書く
a = b;b = 1;
変数を読んだ後、この変数を再度書き込みます。
上記3つの場合、2つの演算の実行順序を入れ替えるだけで、プログラムの実行結果は変わります。
前述したように、コンパイラーとプロセッサーは操作の順序を変更する場合があります。コンパイラとプロセッサは、並べ替えの際にデータの依存関係を尊重し、データの依存関係がある 2 つの操作の実行順序を変更しません。
ここで言及するデータの依存関係は、単一のプロセッサーで実行される命令シーケンスと、単一のスレッドで実行される操作のみを指します。異なるプロセッサーおよび異なるスレッド間のデータの依存関係は、コンパイラーとプロセッサーの考慮事項には影響されません。
as-if-serial セマンティクス
as-if-serial セマンティクスとは、(並列性を向上させるためにコンパイラとプロセッサを) どのように並べ替えても、(シングルスレッドの) プログラムの実行結果は変更できないことを意味します。コンパイラ、ランタイム、およびプロセッサはすべて、as-if-serial セマンティクスに従う必要があります。
as-if-serial セマンティクスに準拠するために、コンパイラーとプロセッサーは、データ依存関係のある操作の順序を変更しません。これは、そのような順序の変更により実行結果が変更されるためです。ただし、操作間にデータの依存関係がない場合、これらの操作はコンパイラーとプロセッサーによって順序変更される可能性があります。具体的な説明については、円の面積を計算する次のコード例を参照してください:
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
上記 3 つの操作のデータ依存関係を下の図に示します:
上の図に示すように、AとCの間にはデータ依存関係があり、同時にBとCの間にもデータ依存関係があります。したがって、最終的に実行される命令列において、C を A および B の前に並べ替えることはできません (C が A および B の前に置かれると、プログラムの結果が変わります)。ただし、A と B の間にデータの依存関係はなく、コンパイラーとプロセッサーは A と B の間の実行順序を並べ替えることができます。次の図は、プログラムの 2 つの実行シーケンスを示しています。
as-if-serial セマンティクスに準拠するコンパイラーの場合、ランタイムとプロセッサーが共同してシングルスレッド プログラムを保護します。シングルスレッド プログラムを作成する人は、シングルスレッド プログラムがプログラムの順序で実行されるかのような錯覚を引き起こします。 as-if-serial セマンティクスにより、シングルスレッド プログラマは並べ替えによる干渉を心配する必要がなく、メモリの可視性の問題を心配する必要もありません。
プログラム シーケンス ルール
プログラム シーケンス ルールに従って、円の面積を計算するための上記のコード例には、次の 3 つの以前の関係があります。 ;
A happens-before C;
ここでの 3 番目の happens-before 関係は、happens-before の推移性に基づいて導出されます。
ここでは A が B の前に発生しますが、実際の実行では、B は A より前に実行できます (上記の並べ替えられた実行順序を参照)。第 1 章で述べたように、A が B の前に発生する場合、JMM は A が B より前に実行される必要はありません。 JMM では、前の操作 (実行の結果) が後続の操作から認識可能であること、および前の操作が 2 番目の操作の順序に先行することのみが必要です。ここで、操作 A の実行結果は操作 B から見える必要はなく、操作 A と操作 B を並べ替えた後の実行結果は、操作 A と操作 B を前発生順序で実行した結果と一致します。この場合、JMM はこの並べ替えは違法ではない (違法ではない) と判断し、JMM はこの並べ替えを許可します。
コンピューターでは、ソフトウェア技術とハードウェア技術には共通の目標があります。それは、プログラムの実行結果を変更せずに、可能な限り多くの並列処理を開発することです。コンパイラーとプロセッサーはこの目標を遵守しています。happens-before の定義から、JMM もこの目標を遵守していることがわかります。
マルチスレッドに対する並べ替えの影響
次に、並べ替えによってマルチスレッドプログラムの実行結果が変わるかどうかを見てみましょう。以下のサンプルコードをご覧ください:
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } Public void reader() { if (flag) { //3 int i = a * a; //4 …… } } }
答えは、必ずしも表示されるわけではありません。
操作 1 と操作 2 にはデータの依存関係がないため、コンパイラーとプロセッサーはこれら 2 つの操作を並べ替えることができます。同様に、操作 3 と操作 4 にはデータの依存関係がなく、コンパイラーとプロセッサーはこれら 2 つの操作の並べ替えも行うことができます。まず、操作 1 と操作 2 を並べ替えると何が起こるかを見てみましょう。以下のプログラム実行タイミング図をご覧ください:
上の図に示すように、操作 1 と操作 2 の順序が変更されています。プログラムが実行されると、スレッド A が最初にフラグ変数 flag を書き込み、次にスレッド B がこの変数を読み取ります。条件が true であるため、スレッド B は変数 a を読み取ります。この時点では、変数 a はスレッド A によってまったく書き込まれておらず、ここでのマルチスレッド プログラムのセマンティクスは並べ替えによって破壊されます。
※注: この記事では、間違った読み取り操作を示すために赤い点線の矢印を使用し、正しい読み取り操作を示すために緑の点線の矢印を使用します。
操作 3 と 4 を並べ替えると何が起こるかを見てみましょう (この並べ替えを利用して、コントロールの依存関係も説明できます)。以下は、オペレーション 3 と 4 が並べ替えられた後のプログラムの実行タイミング図です。
プログラムでは、オペレーション 3 と 4 には制御の依存関係があります。コード内に制御の依存関係がある場合、命令シーケンスの実行における並列度に影響します。この目的を達成するために、コンパイラーとプロセッサーは投機実行を使用して、並列処理に対する制御の依存関係の影響を克服します。プロセッサの投機的実行を例にとると、スレッド B を実行するプロセッサは、事前に a*a を読み込んで計算し、その計算結果をリオーダ バッファ (リオーダ バッファ ROB) と呼ばれるハードウェア キャッシュに一時的に保存できます。次の演算 3 の条件が真と判定された場合、計算結果が変数 i に書き込まれます。
この図から、推測実行では基本的に操作 3 と 4 の順序が変更されることがわかります。ここで、並べ替えはマルチスレッド プログラムのセマンティクスを破壊します。
シングルスレッド プログラムでは、コントロールの依存関係を持つ操作を並べ替えても実行結果は変わりません (これが、as-if-serial セマンティクスでコントロールの依存関係を持つ操作の並べ替えを許可する理由です)。プログラムの実行結果が変わる可能性があります。
上記は Java メモリ モデルの詳細な分析です: コンテンツの並べ替え 詳細については、PHP 中国語 Web サイト (www.php.cn) に注目してください。