最近、自閉症のインタビュアーからの質問が増えているので、「Javaでの並行プログラミングの芸術」を他の記事と組み合わせて読み直し、JMMと揮発性を詳細に解釈することにしました。
1.現代のコンピュータのメモリモデル
実際、初期のコンピュータのCPUとメモリの速度はほぼ同じでしたが、最近のコンピュータでは、CPUの命令速度がメモリのアクセス速度をはるかに上回っています。これは、コンピュータのストレージデバイスとプロセッサの計算速度が数桁の違いがあるため、最近のコンピュータシステムでは、メモリとプロセッサ間のバッファとして、読み取り速度と書き込み速度がプロセッサの処理速度に可能な限り近いキャッシュ層(Cache)を追加する必要があります。
操作に必要なデータをキャッシュにコピーして、操作をすばやく実行できるようにします。操作が終了すると、キャッシュからメモリに同期して、プロセッサが遅いメモリ読み取りを待つ必要がないようにします。書き込み
キャッシュベースのストレージの相互作用は、プロセッサとメモリ間の速度の矛盾をうまく解決しますが、キャッシュコヒーレンス(CacheCoherence)という新しい問題を引き起こすため、コンピュータシステムの複雑さも増します。
マルチプロセッサシステムでは、各プロセッサに独自のキャッシュがあり、同じメインメモリ(MainMemory)を共有します。
現在使用しているCPUは通常マルチコアです。各CPUコアには独自のL1キャッシュとL2キャッシュがあり、複数のCPUコアで共有されるL3キャッシュとメインメモリがあります。CPUキャッシュのアクセス速度はメインメモリよりもはるかに高速であり、CPUキャッシュでは、L1 / L2キャッシュもL3キャッシュよりも高速です。
次に、Javaメモリモデル
Javaでは、すべてのインスタンスフィールド、静的フィールド、および配列要素がヒープメモリに格納され、ヒープメモリはスレッド間で共有されます。ローカル変数、メソッド定義パラメーター、および例外ハンドラーパラメーターはスレッド間で共有されず、メモリの可視性の問題は発生せず、メモリモデルの影響を受けません。
JMM自体は、実際には存在しない抽象的な概念です。一連のルールまたは仕様を記述します。この一連の仕様を通じて、プログラム内のさまざまな変数(インスタンスフィールド、静的フィールド、配列オブジェクトを構成する要素など)のアクセス方法を説明します。 )が定義されています。
Javaスレッド間の通信は、Javaメモリモデル(JMM)によって制御され、JMMは、共有変数へのスレッドの書き込みが別のスレッドに表示されるタイミングを決定します。抽象的観点から、JMMはスレッドとメインメモリ間の抽象関係を定義します。スレッド間の共有変数はメインメモリに格納され、各スレッドにはプライベートローカルメモリがあり、スレッドはローカルメモリに格納されます。コピーの読み取り/書き込み共有変数の。ローカルメモリはJMMの抽象的な概念であり、実際には存在しません。ローカルメモリは、キャッシュ、書き込みバッファ、レジスタ、およびその他のハードウェアとコンパイラの最適化を対象としています。
3、揮発性
1.揮発性の特性
Volatileは、Javaによって提供される軽量の同期メカニズムです。
- 可視性を確保する
- 原子性の保証はありません
- 命令の並べ替えを無効にする
2.CPUキャッシュの一貫性の問題
揮発性変数によって変更された共有変数が書き込まれると、アセンブリコードの2行目があります。ロックプレフィックス付きの命令により、マルチコアプロセッサで次の2つのことが発生します。
- 現在のプロセッサキャッシュラインのデータをシステムメモリに書き戻します
- このメモリへの書き戻し操作により、他のCPUのメモリアドレスにキャッシュされているデータが無効になります。
処理速度を向上させるために、プロセッサはメモリと直接通信せず、最初にシステムメモリ内のデータを内部キャッシュに読み取ってから操作を実行しますが、ライトバック操作はこの変更がいつ書き込まれるかを認識しません。メモリに戻りますが、変数を使用します揮発性書き込み操作の場合、JVMはロックプレフィックス命令をプロセッサに送信して、変数が配置されているキャッシュラインのデータをシステムメモリに書き込みます。
マルチプロセッサでは、各プロセッサのキャッシュの一貫性を確保するために、キャッシュコヒーレンシプロトコルが実装されます。各プロセッサは、バスで送信されたデータをスニッフィングして自身のキャッシュの値をチェックし、その値がキャッシュの有効期限が切れています。プロセッサは、キャッシュラインに対応するメモリアドレスが変更されていることを検出すると、現在のプロセッサのキャッシュラインを無効な状態に設定します。プロセッサがこのデータを変更すると、システムメモリからデータを読み取ります。再度処理するためにキャッシュ内
3.命令再配置の問題
パフォーマンスを向上させるために、コンパイラーとプロセッサーは、特定のコード実行順序で命令を並べ替えることがよくあります。
一般に、並べ替えは次の3つのタイプに分けることができます。
- コンパイラーに最適化された並べ替え。コンパイラーは、シングルスレッド・プログラムのセマンティクスを変更することなく、ステートメントの実行順序を再配置できます。
- 命令レベルの並列並べ替え。最新のプロセッサは、命令レベルの並列テクノロジを使用して複数の命令をオーバーラップさせます。データの依存関係がない場合、プロセッサはステートメントに対応するマシン命令の実行順序を変更できます
- メモリシステムの並べ替え。プロセッサはキャッシュと読み取り/書き込みバッファを使用するため、ロードとストアの操作が順不同で実行されているように見えます。
as-if-serial:どのように並べ替えても、シングルスレッドでの実行結果は変更できません。
コンパイラがバイトコードを生成すると、命令シーケンスにメモリバリアが挿入され(メモリバリアは、メモリ操作の順序を制限するために使用されるプロセッサ命令のセットです)、特定の種類のプロセッサの並べ替えを禁止します。
メモリバリアタイプ:
揮発性書き込み:
各揮発性書き込み操作の前にStoreStoreバリアを挿入すると、揮発性書き込み操作の前に、すべての通常の書き込み操作がすべてのプロセッサに表示されるようになります。これは、StoreStoreバリアにより、上記のすべての通常の書き込みが、揮発性の書き込みの前にメインメモリにフラッシュされることが保証されるためです。
各揮発性書き込み操作の後にStoreLoadバリアを挿入します。その機能は、揮発性書き込みの並べ替えと、それに続く揮発性読み取り/書き込み操作の可能性を回避することです(揮発性変数の書き込み操作が発生する前に、この変数の読み取り操作に対応します)
揮発性の読み取り:
各揮発性読み取り操作の後にLoadLoadバリアを挿入して、プロセッサが上記の揮発性読み取りと以下の通常の読み取りを並べ替えることを禁止します。
各揮発性読み取り操作の後にLoadStoreバリアを挿入して、プロセッサが上記の揮発性読み取りと以下の通常の書き込みを並べ替えることを禁止します。
第四に、アトミック操作の実現原理
1.バスロックを使用して原子性を確保します
バスロックは、プロセッサから提供されたLOCK#信号を使用します。プロセッサがこの信号をバスに出力すると、他のプロセッサの要求がブロックされ、プロセッサは共有メモリを独占できます。
ただし、バスロックはCPUとメモリ間の通信をロックするため、他のプロセッサはロック中に他のメモリアドレスのデータを操作できなくなるため、バスロックのオーバーヘッドは比較的大きくなります。
2.キャッシュロックを使用して原子性を確保します
プロセッサがメインメモリ内の変数の値を更新する場合、変数がCPUのキャッシュラインにある場合、メインメモリへのライトバック操作を実行すると、CPUは他のプロセッサに、キャッシュコヒーレンシプロトコルサーバー上のキャッシュは無効になり、アトミック性を確保するためにメインメモリから再度読み取られます
5、MESI合意
MESIプロトコルは、書き込み無効化と呼ばれるプロトコルです。書き込み失敗プロトコルでは、1つのCPUコアのみがデータの書き込みを担当し、他のコアは書き込みを同期的に読み取るだけです。このCPUコアがキャッシュに書き込まれた後、無効化要求をブロードキャストして、他のすべてのCPUコアに通知します。他のCPUコアは、無効なバージョンのキャッシュラインがあるかどうかを判断し、これを無効としてマークします。
MSEIは、実際には4つの状態の最初の文字の省略形であり、キャッシュラインの4つの状態を表します。
状態 | 説明 |
---|---|
M(変更) | キャッシュラインが有効であり、データが変更されており、メモリ内のデータと矛盾しており、データはこのキャッシュにのみ存在します |
E(Exclusive,独占) | キャッシュラインは有効であり、データはメモリ内のデータと一致しており、データはこのキャッシュにのみ存在します |
S(共有、共有) | キャッシュラインは効果的であり、データはメモリ内のデータと一致しており、データは多くのキャッシュに存在します |
私(無効、無効) | キャッシュラインが無効です |
排他状態のデータは、バスから対応するキャッシュを読み取る要求を受信すると、共有状態になります。この共有状態は、この時点で、別のCPUコアも対応するデータをメモリから独自のキャッシュにロードするためです。
同じデータが複数のCPUコアのキャッシュにあるため、共有状態。したがって、キャッシュ内のデータを更新する場合、直接変更することはできません。代わりに、最初に他のすべてのCPUコアにリクエストをブロードキャストする必要があり、他のCPUコア内のキャッシュを最初に無効な状態にする必要があります。次に、現在のキャッシュのデータを更新します。このブロードキャスト操作は、一般にRFO(Request For Ownership)と呼ばれ、現在の対応するデータの所有権を取得します。
六、起こる-前に
1.発生前の7つのルール
1)プログラムシーケンスルール:スレッドでは、プログラムコードシーケンスに従って、前に書かれた操作が最初に後ろに書かれた操作で発生します。正確には、分岐、ループ、その他の構造を考慮する必要があるため、プログラムコードシーケンスではなく制御フローシーケンスにする必要があります。
2)ロックルールの監視:ロック解除操作は、同じロックでの後続のロック操作で最初に発生します。ここで強調しなければならないのは、同じロックと「背後」は時間の順序を指すということです。
3)揮発性変数の規則:揮発性変数への書き込み操作は、この変数への後続の読み取り操作で最初に発生します。ここで「背後」とは、時間の順序も指します。
4)スレッド開始ルール:Threadオブジェクトのstart()
メソッドは、このスレッドのすべてのアクションで最初に実行されます
5)スレッド終了ルール:スレッド内のすべての操作は、このスレッドの終了検出で最初に発生します。Thread.join()
メソッドThread.isAlive()
の終了や戻り値などの方法で、スレッドが実行を終了したことを検出できます。
6)スレッド中断ルール:中断されたスレッドinterrupt()
のコードが割り込みイベントの発生を検出すると、スレッドメソッドの呼び出しが最初に発生し、このThread.interrupted()
メソッドを使用して中断があるかどうかを検出できます。
7)オブジェクト終了ルール:オブジェクトの初期化の完了(コンストラクターの実行の終了)はfinalize()
、最初にそのメソッドの開始時に発生します
2.発生前の1つの機能:推移性
Aが発生する-Bの前に、Bが発生する-Cの前に、押し出しAが発生する-Cの前に