1。概要
Javaメモリモデルは、Javaメモリモデル(略してJMM)です。抽象的観点から、JMMはスレッドとメインメモリ間の共有変数を定義し、スレッド間 抽象关系
の共有変数はメインメモリに格納され、各スレッドにはプライベートがあり、 工作内存
ワーキングメモリは共有変数の読み取り/書き込み用のスレッドを格納します 副本
。ワーキングメモリはJMMを抽象化したものであり、実際には存在しません。キャッシュ、書き込みバッファ、レジスタ、およびその他のハードウェアとコンパイラの最適化について説明します。
JavaメモリモデルはCPUキャッシュモデルに似ています。JavaメモリモデルはCPUキャッシュモデルに基づいて確立されますが、Javaメモリモデルは標準化されており、下部にある異なるコンピュータ間の違いを保護します。
2.Javaメモリモデルによって引き起こされる問題
Javaメモリモデルは 原子性
、次の8つの操作を含む、メインメモリ上のスレッドの操作を指定します。
ロック:メインメモリ。変数をスレッド専用として識別します。
ロック解除:メインメモリ、スレッド専用変数のロックを解除します。
読み取り:メインメモリ、読み取りメモリからスレッドキャッシュ(ワーキングメモリ);
ロード:作業メモリー、読み取り後の値はスレッドローカル変数コピーに入れられます。
使用:作業メモリー、実行エンジンに値を渡します。
割り当て:作業メモリー、実行エンジンの結果はスレッドローカル変数に割り当てられます。
保存:作業メモリー、書き込みスタンバイ用に値をメインメモリーに保存します。
書き込み:メインメモリ、変数値の書き込み。
次のプログラムで、同期制御のない2つのスレッドが同時にiをインクリメントするとすると、どうなりますか?
public class Test { private int i = 0; public void incremental(){ i ++; System.out.println( "i =" + i); } public static void main(String [] args){ Test t = new Test(); new Thread(()-> t.increment())。start(); new Thread(()-> t.increment())。start(); } }
実行すると、次の3つの状況が発生します
i = 1 i = 1
また
i = 1 i = 2
また
i = 2 i = 2
最初のケースは、次の図で説明されています
スレッドAとスレッドBはどちらも独自のワーキングメモリを持っています。Aは読み取り操作を実行するため、メインメモリからi = 0を読み取り、ロード操作は独自の作業メモリをロードしてから、使用操作を実行してiを自動インクリメントします。 、次に、新しい割り当て操作の割り当てから、スレッドAの作業メモリはi = 1になり、ストア操作が保存され、最後にメインメモリに書き戻され、最後にi=1になります。
スレッドBもこれを行い、読み取り->ロード->使用->割り当て->ストア->書き込み、そして最後にi=1です。
2番目のタイプである重要な点は、値が取得される前に、Bスレッドの読み取り操作がAスレッドからメインメモリに更新されることです。実行シーケンスは次のとおりです。スレッドAの増分->スレッドAはiの最終値を出力します->スレッドBの増分->スレッドBはiの最終値を出力します(以下を参照)。
3番目のタイプは、スレッドAが自己増加後にi = 1をメインメモリに更新することです。印刷を実行する前に、スレッドBは最初にメインメモリからi = 1を取得し、読み取り->ロード->使用->割り当て->を実行します。ストア->書き込み、i=1からi=2にインクリメントすると、スレッドAが印刷操作を実行します。実行順序は次のとおりです。スレッドAの増分->スレッドBの増分->スレッドAはiの最終値を印刷します->スレッドB以下に示すように、i最終値を出力します。
3.可視性、順序、原子性
JavaメモリモデルJMMは、各スレッドに各作業メモリを提供し、共有変数の変数コピーを格納しますが、スレッドが可視性を制御しない場合、上記のプロセスから、マルチでの共有変数の変更がわかります。スレッド化しても、結果はまだ予測できません。
3.1可視性
volatileキーワードは、プログラムレベルで、共有変数への変更が他のスレッドにすぐに表示されるようにします。上記のプログラムは、volatileキーワードをiに追加して、2番目の結果が常に取得されるようにします。
次のプログラムは、デモンストレーションに使用されます。
クラスVolatileExample{ inta = 0; volatile boolean flg = false; public void writer(){ a = 1; flg = true; } public void reader(){ if(flg){ int i = a; ...... } }} }
回路図は以下の通りです:
上記のプロセスは、2つの文にまとめることができます。
当写一个volatile修饰的变量时,JMM会把线程对应的本地内存中的共享变量值刷新的主内存;
当读一个volatile修饰的变量时,JMM会把该线程对应的本地内存置为无效,从主内存读取最新的共享变量的值。
上記の手順は、揮発性の可視性の問題を説明しています。
3.2秩序
一部のコードでは、コンパイラまたはプロセッサは、コード実行の効率を向上させるために、次のコードなどの命令を並べ替えます。
flg = false; //スレッド1: parpare();//リソースを準備します flg= true; //スレッド2: while(!flg){ Thread.sleep(1000); } execute();//準備されたリソースがアクションを実行します
並べ替え後、最初にflag = trueを実行すると、スレッド2は直接待機している間スキップし、特定のコードを実行します。その結果、prepare()メソッドが実行されず、リソースの準備ができていません。このとき、コードロジックが発生します。異常です。
Volatileはメモリバリアを通過して、volatileで変更された変数、および前後に定義されたそれらの値に命令の再配置がないことを保証します。JMMは、次の4つのメモリバリアStoreStore、StoreLoad、LoadLoad、LoadStoreを定義します。
揮発性書き込みの場合、次の図に示すように、StoreStoreを前に挿入し、上記の通常の読み取りと下の揮発性書き込みの並べ替えを禁止します。後でStoreLoadを挿入し、上の揮発性書き込みと下の通常の読み取りの並べ替えを禁止します。
揮発性読み取りの場合は、後でLoadLoadを挿入し、上記の揮発性読み取りと以下の通常の読み取りの並べ替えを禁止します。次にLoadStoreを挿入し、以下に示すように、上記の揮発性読み取りと通常の書き込みの並べ替えを禁止します。
起こる-原則の前に
場合によっては、複数のスレッド間で命令の再配置が発生しないようにするために、Javaメモリモデルでは8つの原則が規定されています。
-
プログラムの順序規則:スレッドでは、コードの順序に従って、前に書き込まれた操作が最初に実行されてから、後ろに書き込まれた操作が実行されます。
-
ロックルールの監視:同じ ロックでの後続のロック操作の前に、最初にロック解除操作が発生し ます。
-
揮発性変数の規則:変数への書き込み操作は、変数の後続の読み取り操作の前に最初に発生します。
-
スレッド開始ルール:Threadオブジェクトのstart()メソッドは、このスレッドのアクションごとに最初に発生します。
-
スレッド終了ルール:スレッド内のすべての操作は、スレッド終了検出で最初に発生します。Thread.join()メソッドの終了とThread.isAlive()の戻り値によって、スレッドが実行を終了したことを検出できます。
-
スレッド割り込みルール:スレッドinterrupt()メソッドの呼び出しは、中断されたスレッドのコードが割り込みイベントの発生を検出したときに最初に発生します。
-
オブジェクトのファイナライズルール:オブジェクトの初期化は、finalize()メソッドの先頭で最初に行われます。
-
推移性:操作Aが操作Bの前に発生し、操作Bが操作Cの前に発生する場合、操作Aは操作Cの前に発生すると結論付けることができます。
3.3アトミシティ
一般に、揮発性で変更された変数はアトミック性を保証できません。たとえば、i ++は複合演算です。最初に読み取り、次に変数の値を変更することはアトミックではありません。
4.揮発性物質の役割
上記の説明から、揮発性には2つの主要な機能があると結論付けることができます。
- スレッドの可視性が保証されています
- 命令の並べ替えを無効にする
5.HotSpotレベルの実装
hsdisツールを使用してJavaアセンブリファイルを表示し、最初にhsdis-amd64.dllを\ jdk1.8 \ jre \ binにダウンロードしてから、VMパラメータ-XX:+ UnlockDiagnosticVMOptions -XX:+PrintAssemblyを設定します。
最終的な実行では、揮発性変数の前に次の情報が追加されます
ロックアドレス$0x0、(%rsp)
以下に示すように:
6.低レベルのCPUハードウェアの実装
上記のプロセスでは、JVM仮想マシン lock前置指令
はデータをCPUに送信して、この変数が配置されているキャッシュラインデータをメインメモリに書き込みます。他のCPUによってキャッシュされた値が古い値である場合、問題が発生します。 。マルチCPU(ここでは複数のコアを指します)では、各CPUは 嗅探
、バス上で伝播されたデータが自身のキャッシュと整合性があるかどうかをチェックし、 缓存一致性协议
最後に複数のCPUの内部キャッシュデータの整合性を確認します。下の図。
仮想マシンのロックプレフィックス命令は、基盤となるハードウェアのキャッシュコヒーレンスプロトコルによって完了します。異なるCPUキャッシュコヒーレンスプロトコルは異なります。MSI、MESI、MOSI、Synapse、Firefly、Dragon、およびIntelCPUのキャッシュコヒーレンスプロトコルがあります。 。それはMESIを介して行われます。
MESIプロトコルを実装するには、2つの専門用語を説明する必要があり flush处理器缓存
ます refresh处理器缓存
。
プロセッサキャッシュをフラッシュします。これは、更新された値をキャッシュ(またはメインメモリ)にフラッシュすることを意味します。キャッシュ(またはメインメモリ)にフラッシュする必要があるため、特別なものを渡すことができます。メカニズムにより、他のプロセッサが更新された値を読み取ることができます。独自のキャッシュ(またはメインメモリ)からの値。フラッシュに加えて、彼はバス(バス)にメッセージを送信して、変数の値が彼によって変更されたことを他のプロセッサに通知します。
プロセッサキャッシュを更新します。つまり、プロセッサ内のスレッドが変数の値を読み取るときに、他のプロセッサのスレッドが変数の値を更新したことがわかった場合、そのキャッシュから変数の値を取得する必要があります。他のプロセッサ(またはメインメモリ)は、この最新の値を読み取り、それを独自のキャッシュに更新します。したがって、可視性を確保するために、最下層はMESIプロトコル、フラッシュプロセッサキャッシュおよびリフレッシュプロセッサキャッシュ、このメカニズムのセット全体によって保証されます。
フラッシュとリフレッシュ、これら2つの操作、フラッシュは、書き込みバッファーにとどまるだけでなく、データをキャッシュ(メインメモリ)に強制的にフラッシュすることです。リフレッシュは、バスをスニッフィングし、変数が変更されていることを検出することです。他の処理から強制されるサーバーのキャッシュ(またはメインメモリ)は、変数の最新の値を独自のキャッシュにロードします。
7.まとめ
この記事では、主にJavaメモリモデルの役割について説明し、基盤となる実装の詳細を保護すると同時に、一連の問題を引き起こし、スレッド間の3つの主要な問題、つまり、順序、可視性、原子性、および揮発性キーワードの変更をもたらします。マルチスレッドでの変数の役割、および最下層の実装方法の予備分析。詳細に分析する場合、これはMESIプロトコル仕様とさまざまなハードウェアの最下層の実装ロジックに依存します。 Intelの操作マニュアルなど。後でさらに深くなる時間があります。