データ競合と逐次一貫性保証
データ競合は、プログラムが正しく同期されていない場合に発生します。 Java メモリ モデル仕様では、データ競合を次のように定義しています:
あるスレッドで変数を書き込み、
別のスレッドで同じ変数を読み取り、
書き込みと読み取りは同期によって順序付けされません。
コードにデータ競合が含まれている場合、プログラムの実行により直感に反する結果が生じることがよくあります (前の章の例の場合と同様)。マルチスレッド プログラムが正しく同期されている場合、そのプログラムはデータ競合のないプログラムになります。
JMM は、正しく同期されたマルチスレッド プログラムのメモリの一貫性について次の保証を行います:
プログラムが正しく同期されている場合、プログラムの実行は逐次一貫性を保ちます。つまり、プログラムの実行結果は次のようになります。一貫したメモリ モデルでの実行結果は同じです (後で説明するように、これはプログラマにとって非常に強力な保証です)。ここでの同期とは、一般的な同期プリミティブ (ロック、揮発性、および最終) の正しい使用を含む、広い意味での同期を指します。
Sequential Consistency Memory Model
Sequential Consistency Memory Model は、コンピューター科学者によって理想化された理論的な参照モデルであり、プログラマーに強力なメモリ可視性の保証を提供します。逐次整合性メモリ モデルには 2 つの大きな特徴があります:
スレッド内のすべての操作はプログラムの順序で実行される必要があります。
(プログラムが同期されているかどうかに関係なく) すべてのスレッドは単一の操作実行順序のみを確認できます。逐次一貫性のあるメモリ モデルでは、すべての操作がアトミックに実行され、すべてのスレッドに即座に表示される必要があります。
逐次整合性メモリ モデルは、プログラマに次のビューを提供します:
概念的には、逐次整合性モデルには、左右に動くスイッチを介して任意のスレッドに接続できる単一のグローバル メモリがあります。同時に、各スレッドはプログラムの順序でメモリの読み取り/書き込み操作を実行する必要があります。上の図から、どの時点でもメモリに接続できるスレッドは最大 1 つであることがわかります。複数のスレッドが同時に実行される場合、図のスイッチング デバイスは、すべてのスレッドのすべてのメモリ読み取り/書き込み操作をシリアル化できます。
より深く理解するために、2 つの模式図を通して逐次整合性モデルの特徴をさらに説明しましょう。
2 つのスレッド A と B が同時に実行されていると仮定します。スレッド A には 3 つの操作があり、プログラム内での順序は A1 -> A2 -> A3 です。スレッド B にも 3 つの操作があり、プログラム内での順序は B1 -> B2 -> B3 です。
これら 2 つのスレッドが正しく同期するためにモニターを使用しているとします。スレッド A は 3 つの操作が実行された後にモニターを解放し、その後スレッド B が同じモニターを取得します。逐次整合性モデルにおけるプログラムの実行効果は、次の図のようになります。
次に、2 つのスレッドが同期していないと仮定します。以下は、この非同期プログラムの実行の概略図です。逐次整合性モデルでは:
非同期プログラムの逐次整合性モデルでは、全体的な実行順序は順序付けされていませんが、すべてのスレッドは一貫した全体的な実行順序のみを確認できます。上図を例にとると、スレッド A と B による実行順序は、B1 -> A1 -> A2 -> B2 -> A3 -> B3 となります。この保証は、逐次一貫性のあるメモリ モデル内のすべての操作がどのスレッドからも即座に認識できる必要があるため実現されます。
しかし、JMMにはそのような保証はありません。 JMM 内で非同期プログラムの全体的な実行順序が狂っているだけでなく、すべてのスレッドから見たオペレーションの実行順序も矛盾している可能性があります。たとえば、現在のスレッドが書き込まれたデータをローカル メモリにキャッシュし、それをメイン メモリに更新する前は、書き込み操作は他のスレッドの観点からは現在のスレッドにのみ表示され、書き込み操作は行われたものとみなされます。現在のスレッドによって操作がまったく実行されていません。現在のスレッドがローカル メモリに書き込まれたデータをメイン メモリにフラッシュした後でのみ、この書き込み操作を他のスレッドが認識できるようになります。この場合、操作が実行される順序は、現在のスレッドと他のスレッドの間で矛盾します。
同期されたプログラムの逐次一貫性の効果
次に、モニターを使用して前のサンプル プログラム ReorderExample を同期し、正しく同期されたプログラムがどのように逐次一貫性を持つかを確認します。
以下のサンプルコードをご覧ください:
class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a; …… } } }
上記のサンプルコードでは、スレッド A が Writer() メソッドを実行した後、スレッド B が Reader() メソッドを実行すると仮定しています。これは適切に同期されたマルチスレッド プログラムです。 JMM 仕様によれば、このプログラムの実行結果は、逐次整合性モデルでのこのプログラムの実行結果と同じになります。以下は、2 つのメモリ モデルでのプログラムの実行タイミングの比較表です。
逐次整合性モデルでは、すべての操作がプログラムの順序で逐次実行されます。 JMM では、クリティカル セクションのコードを並べ替えることができます (ただし、JMM では、クリティカル セクションのコードがクリティカル セクションの外に「エスケープ」することは許可されません。これにより、モニターのセマンティクスが破壊されます)。 JMM は、モニターを終了するときとモニターに入るときの 2 つの重要な時点で特別な処理を実行し、スレッドがこれら 2 つの時点で順次整合性モデルと同じメモリ ビューを持つようにします (具体的な詳細は後で説明します)。スレッド A はクリティカル セクションで並べ替えられていますが、モニターの相互排他的な実行特性により、スレッド B はクリティカル セクションでのスレッド A の並べ替えを「観察」できません。この並べ替えにより、プログラムの実行結果を変えることなく、実行効率が向上します。
ここから、特定の実装における JMM の基本ポリシーがわかります。(正しく同期された) プログラムの実行結果を変更せずに、コンパイラーとプロセッサーの最適化の扉を可能な限り開くというものです。
非同期プログラムの実行特性
同期されていない、または正しく同期されていないマルチスレッド プログラムの場合、JMM は最小限のセキュリティのみを提供します。スレッドの実行時に読み取られる値は、前のスレッドによって書き込まれた値であるか、デフォルトです。値 (0、null、false)。JMM は、スレッド読み取り操作によって読み取られた値が突然出現しないことを保証します。最小限の安全性を実現するために、JVM はヒープ上にオブジェクトを割り当てるときに、まずメモリ空間をクリアしてから、その上にオブジェクトを割り当てます (JVM はこれら 2 つの操作を内部で同期します)。したがって、オブジェクトに事前にゼロ設定されたメモリが割り当てられている場合、ドメインのデフォルトの初期化はすでに完了しています。
JMM は、非同期プログラムの実行結果が逐次整合性モデルのプログラムの実行結果と一致することを保証しません。同期されていないプログラムが逐次整合性モデルで実行されると、通常は順序が狂い、その実行結果は予測できないためです。非同期プログラムの実行結果が両方のモデルで一貫していることを保証しても意味がありません。
逐次整合性モデルと同様に、非同期プログラムが JMM で実行されると、通常は順序が狂い、その実行結果は予測できません。同時に、これら 2 つのモデルにおける非同期プログラムの実行特性には次のような違いがあります:
逐次整合性モデルは、単一スレッド内の操作がプログラムの順序で実行されることを保証しますが、JMM は単一スレッド内の操作がプログラムの順序で実行されることを保証しません。単一スレッドはプログラムの順序で実行されます (上記のクリティカル セクションで正しく同期されたマルチスレッド プログラムの順序変更など)。これについては以前にも述べたのでここでは繰り返しません。
逐次整合性モデルは、すべてのスレッドが一貫した操作実行順序のみを確認できることを保証しますが、JMM はすべてのスレッドが一貫した操作実行順序を確認できることを保証しません。これについては以前にも述べたのでここでは繰り返しません。
JMM は、64 ビット長の double 変数に対する読み取り/書き込み操作のアトミック性を保証しませんが、逐次整合性モデルはすべてのメモリ読み取り/書き込み操作のアトミック性を保証します。
3 番目の違いは、プロセッサ バスの動作メカニズムに密接に関係しています。コンピュータでは、データはバスを介してプロセッサとメモリの間で受け渡されます。プロセッサとメモリ間の各データ転送は、バス トランザクションと呼ばれる一連の手順を通じて完了します。バス トランザクションには、読み取りトランザクションと書き込みトランザクションが含まれます。読み取りトランザクションはメモリからプロセッサにデータを転送し、書き込みトランザクションはプロセッサからメモリにデータを転送します。各トランザクションはメモリ内の 1 つ以上の物理的に連続したワードを読み取り/書き込みします。ここで重要なのは、バスを同時に使用しようとするトランザクションをバスが同期するということです。 1 つのプロセッサがバス トランザクションを実行している間、バスは他のすべてのプロセッサおよび I/O デバイスによるメモリの読み取り/書き込みを禁止します。バスの動作メカニズムを模式図で説明してみましょう:
上図に示すように、プロセッサ A、B、C が同時にバスへのバス トランザクションを開始すると、バス アービトレーションによりプロセッサ A が決定されるとします。調停後の競争に勝ちます (バス調停により、すべてのプロセッサがメモリに公平にアクセスできるようになります)。この時点で、プロセッサ A はバス トランザクションを継続しますが、他の 2 つのプロセッサはメモリ アクセスを再び開始する前に、プロセッサ A のバス トランザクションが完了するまで待つ必要があります。プロセッサ A がバス トランザクション (バス トランザクションがリード トランザクションであるかライト トランザクションであるかに関係なく) を実行しているときに、プロセッサ D がバスに対してバス トランザクションを開始するとします。このとき、プロセッサ D のリクエストはバスによって禁止されます。 。
これらのバスの動作メカニズムは、すべてのプロセッサのメモリへのアクセスをシリアル化して実行できます。どの時点でも、メモリにアクセスできるのは最大 1 つのプロセッサだけです。この機能により、単一バス トランザクション内のメモリ読み取り/書き込み操作がアトミックであることが保証されます。
一部の 32 ビット プロセッサでは、64 ビット データの読み取り/書き込み操作をアトミックにする必要がある場合、比較的大きなオーバーヘッドが発生します。この種のプロセッサを処理するために、Java 言語仕様では、JVM が 64 ビット長変数および double 変数の読み取り/書き込みのアトミック性を持つことを推奨していますが、必須ではありません。 JVM がそのようなプロセッサ上で実行される場合、64 ビット長/double 変数の読み取り/書き込み操作が 2 つの 32 ビット読み取り/書き込み操作に分割されて実行されます。これら 2 つの 32 ビット読み取り/書き込み操作は、実行のために異なるバス トランザクションに割り当てられる場合があります。このとき、この 64 ビット変数の読み取り/書き込みはアトミックではありません。
単一のメモリ操作がアトミックでない場合、予期しない結果が生じる可能性があります。下の図を見てください:
上の図に示すように、プロセッサ A が Long 変数を書き込み、プロセッサ B がこの Long 変数を読み取ろうとしているとします。プロセッサ A の 64 ビット書き込み操作は 2 つの 32 ビット書き込み操作に分割され、2 つの 32 ビット書き込み操作は実行のために異なる書き込みトランザクションに割り当てられます。同時に、プロセッサ B の 64 ビット読み取り操作は 2 つの 32 ビット読み取り操作に分割され、2 つの 32 ビット読み取り操作は同じ読み取りトランザクションに割り当てられて実行されます。プロセッサ A と B が上図のタイミング シーケンスに従って実行すると、プロセッサ B には、プロセッサ A によって「半分書き込まれた」だけの無効な値が表示されます。
上記は Java メモリ モデルの詳細な分析です: 逐次一貫性の内容 さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。