JMM とメモリ障壁について深く考える

ベース

メモリバリアと JVM 内でのそのアプリケーション

メモリバリア関連の問題

StoreLoad バリアを使用した後、データがメモリに書き戻された後、キャッシュ ラインに再ロードされる場合がありますが、これが storeLoad に時間がかかる理由ですか?

StoreLoad メモリ バリアは最も高価なメモリ バリアであり、主にマルチプロセッサ環境における「Store-Load」の並べ替え問題を解決し、その一貫性を確保することを目的としています。

StoreLoad バリアは、書き込みキャッシュをフラッシュし、以前のすべての書き込みが完了するまで後続の読み取りをブロックすることにより、バリア前のすべての書き込みがバリア後のすべての読み取りよりも前に完了することを保証します。

このプロセスでは、以前のすべての書き込みが完了するまで待機する必要があるため、プロセッサが停止する可能性があります。これには、書き込みキャッシュからメイン メモリへのデータの書き込みや、場合によってはメイン メモリからのデータの再ロードが含まれる場合があります。このプロセスにかかる待ち時間は主に、キャッシュ操作 (読み取り/書き込み) と比較してメモリ操作 (読み取り/書き込み) の待ち時間が長いことと、プロセッサとメイン メモリ間の通信が必要になる可能性があり、プロセッサの待機時間が長くなることが原因です。時間。

したがって、StoreLoad バリアの時間のかかる部分の一部は、必要となる可能性のあるキャッシュ ラインへのデータの再ロードに起因します。しかし、より重要なのは、書き込みキャッシュ内のデータをメイン メモリにフラッシュするなど、以前のすべての書き込み操作が完了するまで待つ必要があるため、プロセッサはこれらの操作が完了するまで待たなければなりません。

StoreStore もキャッシュ ラインのデータをメイン メモリにフラッシュする必要があるのではないでしょうか?

StoreStore メモリ バリアでは、バリア後のストア操作の前に、以前のストア (書き込み) 操作がすべて完了している必要があります。これは、場合によっては実際にキャッシュ ラインからメイン メモリにデータをフラッシュする必要があることを意味します。ただし、その動作方法と StoreLoad バリアのパフォーマンスへの影響にはいくつかの違いがあります。

StoreStore バリアの主な目的は、ストア操作の並べ替えを防止し、それによって書き込みの順序を保証することです。たとえば、2 つの書き込み操作 W1 と W2 がある場合、StoreStore バリアの後の書き込み操作は W1 または W2 の前に並べ替えられません。

ただし、StoreStore バリアは通常、すべてのデータを一度にメイン メモリにフラッシュする必要はありません。代わりに、後続のストア操作を実行する前に、以前のすべてのストア操作が完了することを単に必要とする場合もあります。これは、データがメイン メモリにすぐにフラッシュされずに、書き込みバッファにまだ存在する可能性があることを意味します。

したがって、StoreStore バリアにキャッシュ ラインからメイン メモリへのデータのフラッシュが含まれることは事実ですが、これは必ずしも必要なわけではなく、バリアの主な目的でもありません。したがって、StoreStore バリアは通常、StoreLoad バリアよりもパフォーマンスへの影響が小さくなります。

ロードストアについてはどうですか?

LoadStore メモリ バリアは、バリア前のすべてのロード (読み取り) 操作が、バリア後のすべてのストア (書き込み) 操作より前に完了することを保証します。これは主に、読み取りおよび書き込み操作の順序変更を防ぐためです。

LoadStore メモリ バリアはキャッシュからメイン メモリにデータをフラッシュする必要はありませんが、書き込み操作を実行する前にすべての読み取り操作が完了することを保証します。これは通常、読み取り操作が完了し、書き込み操作が実行される前にデータがキャッシュにロードされたことを意味します。

したがって、LoadStore メモリ バリアは、データがキャッシュからメイン メモリにフラッシュされるのを待つ必要がないため、通常、StoreStore および StoreLoad メモリ バリアよりもパフォーマンスへの影響は小さくなります。

特定の実装の詳細とパフォーマンスへの影響は、プロセッサのアーキテクチャやメモリ モデル、JVM の特定の実装などの要因によって異なる場合があることに注意してください。

MESI の StoreLoad に関する限り、Store は他の CPU のキャッシュを無効にするように動作し、現在のキャッシュ ラインのデータをメモリに書き戻します。Load は、他の CPU が新しく書き込まれたデータをメイン メモリから取得することを意味します。

あなたの理解は基本的に正しいです。MESI (Modified、Exclusive、Shared、Invalid) プロトコルでは、プロセッサ コアが書き込み (Store) 操作を実行するとき、最初に他のコアに対応するキャッシュ ラインのステータスを無効 (Invalid) にしてから、ローカル キャッシュ ラインを変更します。このキャッシュ ラインのデータをメイン メモリに書き戻す必要があると、キャッシュ ラインのステータスが変更済みから排他に変わり、キャッシュ ラインがメイン メモリ内のデータと一致していることを示します。

次の読み取り (ロード) 操作で、他のプロセッサ コアがこのデータを読み取る必要がある場合、書き込まれたばかりのデータをメイン メモリから独自のキャッシュ ラインにロードし、キャッシュ ラインの状態を更新して共有に設定します。

StoreLoad バリアは、読み取り前に書き込みを完了 (メイン メモリへの必要なデータ同期を含む) できるようにすることで、1 つのコアからの書き込みが他のコアからの読み取りから見えるようにします。このプロセスには、書き込み、他のコアのキャッシュ ラインの無効化、メイン メモリへのデータの同期、メイン メモリからのデータのロードなど、複数のメモリ操作が含まれる場合があるため、StoreLoad が最もコストのかかるメモリ バリアとなります。

このプロセスはハードウェアによって自動的に処理され、通常、プログラマはこのプロセスを直接制御しない(制御できない)ことに注意してください。同時に、最新のプロセッサーとオペレーティング システムの最適化により、実際のプロセスは上記の説明よりも複雑で効率的になる可能性があります。

MESIとメモリバリアの関係は? JMM のメモリ バリアは、変数の可視性と順序を維持する抽象化に属し、MESI はその特定の実装の一種であり、特定のシナリオに適用されます。

Java メモリ モデル (JMM) のメモリ バリアは、マルチスレッド環境におけるメモリ操作 (読み取り/書き込み) の可視性と順序付けを確保するための抽象化メカニズムです。MESI (Modified、Exclusive、Shared、Invalid) プロトコルは、マルチコア プロセッサ システムでキャッシュ コヒーレンスを実現するための特別なメカニズムです。

MESI プロトコルの動作シナリオはマルチプロセッサ (またはマルチコア) システムです。このようなシステムでは、各プロセッサ (またはコア) に独自のキャッシュがあります。複数のプロセッサが同じメモリを同時に操作すると、あるプロセッサのキャッシュの値が変更されているのに、他のプロセッサのキャッシュの値が古いままになる可能性があり、いわゆるキャッシュの不整合の問題が発生します。この問題を解決するために MESI プロトコルが提案されています。

MESI プロトコルでは、各キャッシュ ラインには変更済み、排他的、共有、無効の 4 つの状態があります。これら 4 つの状態遷移を通じて、MESI プロトコルは各プロセッサが最新のデータを参照できるようにします。

JMM の場合、最下層がどのように実装されているかは気にせず、メモリ操作の可視性と順序を確保するためのメカニズム (つまり、メモリ バリア) があることを知っていればよいだけです。最下位層では、この仕組みを実現するために、MESIのようなプロトコルが使用される場合がある。したがって、メモリ バリアは、上位層 (JMM) と下位層 (MESI) の間の「ブリッジ」と考えることができます。

つまり、JMM メモリ バリアを実現するための具体的な手段としては MESI プロトコルが考えられますが、これが唯一の方法ではなく、他のハードウェア メカニズムやプロトコルが存在する可能性があります。これは、特定のハードウェアとオペレーティング システムの実装によって異なります。

シングルトン モードと揮発性メモリ バリアの DCL 実装

volatile キーワードを使用しない DCL の実装に順序変更のリスクがあるのはなぜですか?

  1. DCL (Double Checked Locking) のコード例は次のとおりです。
 private static Singleton instance;

    public static Singleton getInstance() {
    
    
        if (instance == null) {
    
       // first check
            synchronized(Singleton.class) {
    
    
                if (instance == null) {
    
       // second check
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    private Singleton() {
    
    
        // initialize
    }
}

このコードの jvm 命令はおおよそ次のとおりです。

0: getstatic     #2      // Field instance:LSingleton;
3: ifnonnull     32     // if instance is not null, jump to 32
6: ldc           #3     // Class reference to Singleton
8: dup           // Duplicate the class reference
9: astore_1      // Store the reference in local variable 1
10: monitorenter  // Enter synchronized block
11: getstatic     #2    // Field instance:LSingleton;
14: ifnonnull     22   // if instance is not null, jump to 22
17: new           #3   // Create new Singleton
20: putstatic     #2   // Assign to instance
21: aload_1       // Load the reference from local variable 1
22: monitorexit   // Exit synchronized block
23: goto          32   // Jump to 32
24: astore_2      // Exception handling: store the exception
25: aload_1       // Load the reference from local variable 1
26: monitorexit   // In case of an exception, exit synchronized block
27: aload_2       // Load the exception
28: athrow        // Re-throw the exception
29: getstatic     #2  // Field instance:LSingleton;
32: areturn       // Return the instance

このバイトコードでは、new 命令を使用して新しい Singleton オブジェクトを作成し、putstatic 命令を使用して新しく作成されたオブジェクトの参照をインスタンス フィールドに割り当てます。ただし、ここには重要な問題があります。問題は、Java メモリ モデルの機能で、適切な同期がないと、あるスレッドによるオブジェクト (ここではシングルトンのインスタンス化) への書き込みが他のスレッドから見えなかったり、部分的に初期化されたオブジェクトが他のスレッドから見えたりする可能性があります。

**上記のバイトコードでは、主な問題は new 命令と putstatic 命令の間で発生します。「コンストラクター トラバーサル」と呼ばれる問題があります。つまり、new と putstatic の 2 つの命令の順序が JVM によって並べ替えられる可能性があります。具体的には、JVM は最初に putstatic 命令を実行し、完全に初期化されていないシングルトン オブジェクトをインスタンス フィールドに割り当て、次に新しい命令を実行してオブジェクトを初期化します。マルチスレッド環境では、この時点で別のスレッドが getInstance() メソッドを実行すると、完全に初期化されていないシングルトンを取得する可能性があります。

DCL メカニズムのような volatile キーワードを使用する場合、一般にどのようなバリアが使用されますか? (バイト面接の質問: ll ls を前後に置くことの効果は、実際にはvolatile変数を読み取る際のメモリ バリアの使い方に相当します)

Java では、遅延初期化とシングルトン パターンを実装するために、ダブル チェック ロック (DCL、ダブル チェック ロック) パターンがよく使用されます。このモードでは、volatile通常、オブジェクトの初期化の原子性と可視性を確保するためにキーワードが使用されます。具体的には、これは Java のメモリ モデルがvolatile変数の読み取りと書き込みに対して特別なメモリ バリアを提供しているためです。

  • 揮発性変数に対する書き込み操作の場合、書き込み操作の後にStoreStore バリアが挿入され、後続の書き込み操作が揮発性書き込み操作の前に並べ替えられるのを防ぎます。また、後続の読み取りおよび書き込み操作が揮発性変数に並べ替えられるのを防ぐために StoreLoad バリアも挿入されます。書き込み操作の前。

  • volatile 変数の読み取り操作の場合、 volatile読み取り操作の後に前の読み取り操作が並べ替えられるのを防ぐために、LoadLoad バリアが読み取り操作の後に挿入されます。また、前の読み取り操作が順序変更されるのを防ぐために LoadStore バリアも挿入されます。書き込み操作の前に後続のものに並べ替えられます。

DCL モードでは、volatileこのキーワードにより、シングルトン オブジェクトの初期化時にオブジェクトのすべてのフィールドが正しく初期化され、この初期化プロセスがすべてのスレッドに表示されることが保証されます。これは主に、前述の StoreStore および StoreLoad メモリ バリアを通じて実現されます。

同時に、volatileこのキーワードは、シングルトン オブジェクトが初期化されているかどうかをチェックするときに、オブジェクトが初期化されている場合、volatileこのチェック操作後のすべての操作 (つまり、変数の読み取り) でこのオブジェクトの正しい状態を確認できることも保証します。これは主に、前述の LoadLoad および LoadStore メモリ バリアを通じて実現されます。

これらのメモリ バリアが、揮発性変数の読み取りおよび書き込みの前ではなく、後に挿入されるのはなぜですか? (重要)

Java メモリ モデルの volatile キーワードのセマンティクスでは、メモリ バリアの設定には特定のロジックがあります。

揮発性変数への書き込みの場合、書き込み後にメモリ バリアが挿入されます。その理由は、volatile 変数が書き込まれるときに、他のスレッドが最新の値を参照できるようにするためです。つまり、挿入された StoreStore バリアと StoreLoad バリアにより、書き込まれた揮発性変数が後続の操作で並べ替えられず、他のスレッドが最新の値を確認できるようになります。

揮発性変数の読み取り操作の場合、読み取り操作の後にメモリ バリアが挿入されます。理由は、最新の値を読み取れるようにしたいからです。挿入された LoadLoad バリアと LoadStore バリアは、読み取り操作の前に読み取り操作と書き込み操作が読み取り操作後に再配置されるのを防ぎ、最新の値が確実に読み取られるようにします (なぜ後続の書き込み操作の前に読み取り操作が読み取られないようにするのですか? 最新の内容については、すべて読み込まれていて、書き込みがないのではありませんか?これは、Load はメインメモリからキャッシュにロードされる実際のデータであり、通常読み込まれるデータはキャッシュ内の古いデータである可能性があるためです)。

一般に、volatile キーワードの場合、メモリ バリアの挿入位置はそのセマンティクスを保証するためのものです。スレッドが volatile 変数に書き込むと、他のスレッドは書き込み操作の効果をすぐに確認できます。つまり、volatile 変数が実現されます。視認性。同時に、 volatile キーワードは、コンパイラが volatile 変数に関連するコードの順序を変更することを防ぎ、それによって命令の実行順序がプログラマの期待どおりになること、つまり volatile 変数の順序が実現されることを保証します。

揮発性変数間の演算を再配置することはまだ可能ですか?

揮発性変数自体の読み取り/書き込み操作は並べ替えられませんが、これはすべての操作が並べ替えられないという意味ではありません。たとえば、揮発性変数間の演算は依然として並べ替えられる可能性がありますvolatile 変数に対する読み取り/書き込み操作と別の volatile 変数に対する書き込み/読み取り操作の間にデータの依存関係がある場合にのみ、2 つの操作は並べ替えられません。

volatile がアトミック操作をサポートしないのはなぜですか?

なぜ volatile は原子性を保証できないのでしょうか?

volatile は変数 a を変更します。マルチスレッドが i++ を呼び出すとき、メモリ バリアはいくつ挿入されますか?

volatileキーワードは変数の可視性と順序付けを保証しますが、原子性は保証しません。volatileマルチスレッド環境で変更された変数に対して i++ 操作 (値の読み取り、値の増加、値の書き戻しの 3 つのステップを含む) を実行する場合、これらの 3 つのステップはアトミックな操作ではないため、他のスレッドによって中断される可能性があります

メモリ バリアに関して、Java メモリ モデルは、volatile変数書き込み操作の場合、JVM は書き込み操作の後に StoreStore バリアと StoreLoad バリアを挿入することを規定しています。これら 2 つのバリアにより、書き込み操作の順序と可視性が確保されます。変数読み取り操作の場合、JVMvolatileは読み取り操作の後に LoadLoad バリアと LoadStore バリアを挿入すると、これら 2 つのバリアによって読み取り操作の順序と可視性が確保されます。

マルチスレッド環境でのvolatile変更された変数に対する i++ 操作の場合:

  1. まず、読み取り操作を実行して現在の値を読み取り、読み取り操作の後に LoadLoad バリアと LoadStore バリアを挿入します。

  2. 次に値を増やす操作が行われますが、値を増やす操作自体は変数を直接操作するものではないため、メモリバリアは挿入されませんvolatile

  3. volatile最後に、値を書き戻す操作が実行され、増加した値が変数に書き戻され、書き込み操作の後に StoreStore バリアと StoreLoad バリアが挿入されます。

したがって、volatile変更された変数に対する i++ 操作では、4 つのメモリ バリアが挿入される可能性があります。しかし、i++ 操作はアトミック操作ではなく、マルチスレッド環境では依然としてデータの不整合が発生する可能性があるため、これでも同時実行性の問題は解決できません。AtomicIntegerこの問題を解決する必要がある場合は、 orsynchronizedキーワードを使用して i++ 操作の原子性を実現することを検討できます。

volatile で修正された i++ が CPU、キャッシュ、メイン メモリでのデータの読み書きに関してアトミック性を保証できない例を挙げていただけますか?

もちろん。2 つのスレッド A と B があり、どちらもvolatileVariable++操作を実行しようとしているとします。以下のような実行が考えられます。

  1. スレッド A は、volatileVariable値が 1 であると仮定して、変数をメイン メモリから自身の作業メモリに読み取ります。

  2. 同時に、スレッド B もvolatileVariableメイン メモリから自身の作業メモリに変数を読み取ります。この変数の値も 1 です。

  3. スレッド A は、自身の作業メモリで +1 操作を実行しますが、volatileVariable現在は 2 です。

  4. スレッド A は、volatileVariable新しい値 2 をメイン メモリに書き込みます。

  5. スレッド B も、自身の作業メモリ内で +1 操作を実行します。作業メモリはvolatileVariable現在 2 です (ただし、3 である必要があります)。

  6. スレッド B は、volatileVariable新しい値 2 をメイン メモリに書き込みます。

volatile を使用すると、スレッド B はステップ 1 のステップ 3 でスレッド A によって読み取られた最新の値を確認できるようになりますが、スレッド A には新しい値を書き戻す時間がないため、スレッド B はすでにその操作の実行を開始しており、これにより次のような問題が発生します。 i++ 操作の非アトミック性が保証されています。

したがって、たとえvolatileVariablevolatile で変更されたとしても、同時実行環境では非アトミックな問題が発生します。理論的にvolatileVariableは 3 であるはずですが、実際には 2 になります。volatileVariable++volatile は変数の可視性と順序付けを保証できますが、のような複合操作のアトミック性は保証できません。

この場合、通常は、AtomicIntegersynchronizedキーワードなどのアトミック クラスを使用して、操作のアトミック性を確保します。

ここでのスレッド ワーキング メモリは抽象的な概念であり、ハードウェア上のレジスタやキャッシュに直接対応するものではないことに注意してください。実際には、スレッドが使用するメモリ領域を示すために使用されます。この領域には、CPU キャッシュ、レジスタが含まれる場合があり、その他のハードウェアやコンパイラの最適化も含まれる場合があります。

スレッドがメイン メモリから自身の作業メモリに変数を読み取るとは、スレッドがこの変数の最新の値を取得し、この最新の値をアクセス可能なメモリ領域に保存することを意味します。具体的には、このメモリ領域がハードウェア上でどのように実装されるかは、Java プログラムにとって透過的です。Java メモリ モデルは、読み取りおよび書き込み操作がハードウェア上でどのように実装されるかではなく、読み取り操作と書き込み操作の順序と可視性に関係します。

つまり、揮発性書き込みがすぐにメモリにフラッシュされたとしても、リフレッシュの前に (たとえば、レジ​​スタがまだ自動インクリメントしているとき、別のスレッドがすでに値をフェッチしている)、スレッドが存在する可能性があります。それはメモリ内の古い値を読み取ります。たとえば、CPU が変数を変更する前に、スレッドは古い値を読み取ります。

はい、あなたの理解は正しいです。volatile キーワードを使用すると、スレッドが volatile 変数に書き込んだ後、変更が直ちにメイン メモリにフラッシュされます。ただし、リフレッシュ操作が完了する前に別のスレッドがこの変数の値を読み取った場合、このスレッドは引き続き古い値を読み取ります。

これが、たとえ操作の変数が揮発性であっても、「i++」の複合操作 (読み取り、変更、ライトバック) がマルチスレッド環境ではスレッドセーフではない理由です。「i++」の読み取りと書き込みの間で、別のスレッドが古い値を読み取り、それに基づいて動作する可能性があり、その結果、誤った結果が生じる可能性があります。この問題を回避するには、他の同期メカニズム (ロックやアトミック変数など) を使用して、この複合操作のスレッドの安全性を確保する必要があります。

スレッド A には新しい値を書き戻す時間がなかったため、スレッド B はすでにその操作の実行を開始していますが、なぜこのようなことが起こるのでしょうか?

最新のプロセッサはマルチコアおよびマルチ CPU であるため、各 CPU には独自のレジスタとキャッシュ ラインがあり、スレッドの実行ステータスは異なります。

おすすめ

転載: blog.csdn.net/yxg520s/article/details/131897150