序文
今日はボラティリティについて話してください。頻繁なインタビューサイト、それは同期しているように感じます。
volatileは可視性と順序を保証しますが、原子性を保証するものではないことは誰もが知っています。原子性を保証するには、同期などのロックメカニズムを使用する必要があります。したがって、揮発性を理解するために、主にこれら3つの特性に焦点を当てます。
JMM
揮発性を学ぶ前に、JMMを理解する必要があります。
JMM Javaメモリモデルは、実際には存在しない抽象的な概念であり、プログラム内のさまざまな変数のアクセス方法を定義するための一連のルールまたは仕様を記述します。
同期に関するJMM規制:
- スレッドのロックを解除する前に、共有変数の値をメインメモリにフラッシュバックする必要があります
- スレッドがロックする前に、メインメモリの最新の値を自身の作業メモリに読み取る必要があります
- ロックとロック解除は同じロックです
プログラムを実行しているJVMのエンティティはスレッドであり、各スレッドが作成されると、JVMはそのための作業メモリを作成します。作業メモリは各スレッドのプライベートデータ領域です。JMMはすべての変数がメインメモリに格納されます。メインメモリは共有メモリ領域です。すべてのスレッドがアクセスできますが、変数に対するスレッド操作は作業メモリーで実行する必要があります。まず、バイクをメインメモリからそれ自体の作業メモリスペースにコピーし、次に変数を操作し、操作が完了した後に変数をメインメモリに書き戻します。メインメモリ内の変数を直接操作することはできません。各スレッドの作業メモリーは、変数のコピーをメインメモリーに保管します。したがって、異なるスレッドは互いの作業メモリーにアクセスできず、スレッド間の通信はメインメモリーを介して完了する必要があります。
JMMの8つの操作
- 読み取り:メインメモリからデータを読み取ります
- ロード:メインメモリから読み取ったデータを作業メモリに書き込みます
- ストア:ワーキングメモリデータをメインメモリに書き込みます
- 書き込み:ストア内の変数の値をメインメモリ内の変数に割り当てます
- 使用(使用):作業メモリーからデータを読み取って計算します
- 割り当て(割り当て):計算された値を作業メモリーに再割り当てします
- ロック:メインメモリ変数をロックし、スレッド排他状態としてマークします
- ロック解除(ロック解除):メインメモリ変数のロックを解除します。他のスレッドのロックを解除すると、変数をロックできます。
volatileを設定している限り、整合性プロトコルはバスをロックする必要はありません。その場合、キャッシュの無効化をトリガーするためのロック命令があります。
フローチャートを通じて、上記のコードの2つのスレッドの実行フローを大まかに分析できます。
スレッド1:
- メインメモリのデータを読み取りたい。
- ワーキングメモリにデータをロードする
- CPUはデータを使用します
スレッド2:最初の3つのステップは変更されません。4.計算された値を作業メモリーに再割り当てします。5。作業メモリーのデータをメインメモリーに直接書き込みます。6。ストアの過去の変数の値をメインメモリー内の変数に割り当てます。
ただし、メインスレッドの変数initFlagは変更されていますが、スレッド1は変更のシグナルを受信していないため、initFlagが変更されない前の状態になっていることがわかります。
では、スレッド間で変数を表示したい場合はどうでしょうか。
可視性
可視性について言えば、MESIキャッシュコヒーレンシプロトコルについて言及する必要があります。
MESIキャッシュコヒーレンスプロトコル:複数のCPUメインメモリがそれぞれのキャッシュに同じデータを読み取ります。CPUの1つがキャッシュ内のデータを変更すると、データはすぐにメインメモリに同期されます。他のCPUはバススニッフィングメカニズムを使用できます。データの変更を認識し、独自のキャッシュ内のデータを無効にします。
では、このバススヌーピングメカニズムとは何ですか?各CPUはバスを監視します。スレッドの1つが変数の値を変更する場合、メインメモリに書き戻すパスでバスを渡す必要があります。次に、他のスレッドがバスを監視していて、変更された値が変更されたことを検出すると、作業メモリの変数値を空にしてから、メインメモリに移動して変数値を読み取ります。
揮発性キャッシュ可視性実装原理アセンブリ命令ロック
- 現在のプロセッサキャッシュラインデータはすぐにメインメモリに書き戻されます
- この書き込み操作により、バススヌーピングメカニズム(MESIプロトコル)がトリガーされます。
その可視性の実現原理は、基礎となる実装が主にLockプレフィックス命令のアセンブリを介することであると説明します(変数はvolatileで追加されます。変更操作時に、基礎となるアセンブリは行にロックを追加します)。このメモリ領域のキャッシュをロックし(キャッシュラインをロック)、メインメモリに書き戻します。他のスレッドの作業メモリは常にバスを監視します。この変数の変更を監視すると、他のCPUにキャッシュされているメモリアドレスのデータが無効になります(MESIプロトコル)。値を取得したい場合は、メインメモリに移動して再度取得する必要があります。
原子性の保証はありません
アトミック性について説明する前に、小さなコードを実行してみましょう。
public class Test { public static volatile int num = 0; public static void incremental (){ num ++; } public static void main(String [] args)throws InterruptedException { Thread [] thread = new Thread [10]; for(int i = 0; i <threads.length; i ++){ threads [i] = new Thread(new Runnable(){ public void run(){ for(int j = 0; j <1000; j ++){ increase (); } } }); スレッド[i] .start(); } for(Thread t:threads){ t.join(); } System.out.println(num); } }
常識に従ってコードを考えると、1つのスレッドがnum + 1を与えた後、その結果がメインスレッドに書き込まれ、他のスレッドはnumがMESIを介して変更されたことを監視し、メインメモリのnum。値を入力し、1を加算すると、結果は1000になります。これは本当に本当ですか?操作の結果を見てみましょう。
時々それは本当です。しかし、それをさらに数回実行すると、それでも異なることがわかります。
なぜそれが欠けているのですか?これは、揮発性の保証されていない原子性です。次に、理由を分析します。
最初のスレッド1はnumに1を追加し、次にメインメモリに書き戻します。numはvolatileによって変更されるため、numが変更されると他のスレッドがリッスンします。スレッド1がnum = 0を与えると、1が追加され、スレッド2もnum + 1を与えると仮定します。このとき、スレッドがメインメモリに書き込み、スレッド2がnumが変更されたことを検出するとすぐに、作業メモリ内のnumを空にします。このとき、メインメモリに移動してデータを読み取り、numにもう一度1を追加します。次に、問題はここにあります。
値は2になりましたが、すでに3回追加しています。元々3回追加すると3回ですが、3回追加すると2になります。これが、結果が1000未満である理由です。
原子性を確保しますか?単純に、numの前にsychronizedを追加します。
注文の再配置
コンピュータがプログラムを実行しているとき、パフォーマンスを向上させるために、コンパイラとプロセッサは命令を並べ替えることがよくあります。命令は一般に次の3つのタイプに分けられます。
ソースコード->コンパイラ最適化再配置->並列命令再配置->メモリシステム再配置->最終的に実行された命令
その中で、プログラムの最終的な実行結果が、シングルスレッド環境でのコードの順次実行の結果と一致していることを確認してください。マルチスレッド環境では、スレッドは交互に実行されます。コンパイラの最適化された再配置により、2つのスレッドで使用される変数が一貫性を保証できるかどうかを判断できず、結果を予測できません。プロセッサは、並べ替えるときに命令間のデータ依存関係を考慮する必要があります。
揮発性の実装は、命令の再配置の最適化を禁止し、それによってマルチスレッド環境でのアウトオブオーダー実行を回避します。
最初に概念を理解します。メモリバリア(メモリバリア)はCPU命令であり、次の2つの機能があります。
- 特定の操作の実行順序を維持する
- 特定の変数のメモリの可視性を維持する
コンパイラとプロセッサの両方が命令再配置の最適化を実行できるためです。メモリバリアが命令の間に挿入されると、コンパイラとCPUに、このメモリバリア命令では命令を並べ替えることができないことを通知します。つまり、メモリバリアを挿入することにより、メモリバリアの前後の命令は禁止されます。並べ替えの最適化を実行します。メモリバリアのもう1つの機能は、さまざまなCPUのキャッシュデータを強制的にフラッシュすることです。これにより、CPU上のすべてのスレッドがこれらのデータの最新バージョンを読み取ることができます。
このため、Javaメモリモデルで採用されている戦略は、各揮発性書き込み操作の前にStoreStoreバリアを挿入することです。各揮発性書き込み操作の後にStoreLoadバリアを挿入します。各揮発性読み取り操作の後にLoadLoadバリアを挿入します。各揮発性読み取り操作の後にLoadStoreバリアを挿入します。
書き込み操作
読み取り操作
バリアを介して、他のコードがメモリバリア内のコードに干渉しないようにすることができます。
総括する
今日は揮発性を紹介しましたが、可視性、秩序、非保証原子性という3つの重要なポイントがあります。可視性は、JMMモデルの可変アクセス方法の規制とMESI(Cache Consistency Protocol)スニッフィングメカニズムによって実現されます。秩序は、アセンブリレベルで秩序を確保するために必要なコードの前後に、メモリバリアを通過します。コードの整然とした実行。最後に、ループがi ++に類似している場合、volatileはアトミック性を保証しないことが分析されます。