同時プログラミングにおける原子性、可視性、順序の問題を解決するために、Java 言語では、 、 、 など、同時処理に関連する一連のキーワードが提供されてsynchronized
いvolatile
ます。[前][2]の記事では、 の使い方と原理についても紹介しました。この記事では、別のキーワード - を分析してみましょう。final
concurren包
synchronized
volatile
この記事はvolatile
これを中心に展開し、主にvolatile
使用法、volatile
原則、volatile
可視性と順序保証を提供する方法を紹介します。
volatile
このキーワードは Java 言語だけでなく多くの言語に存在し、使い方や意味も異なります。特にC言語、C++、Javaにはキーワードがありますvolatile
。どちらも変数またはオブジェクトの宣言に使用できます。以下に、Java 言語のキーワードを簡単に紹介しますvolatile
。
揮発性の使用法
volatile
これは通常「軽量synchronized
」と比較され、Java 並行プログラミングではより重要なキーワードでもあります。異なるのsynchronized
は、volatile
変数修飾子であり、変数を変更するためにのみ使用できます。メソッドとコード ブロックは変更できません。
volatile
の使用法は比較的簡単で、複数のスレッドによって同時にアクセスされる可能性のある変数を宣言する場合にvolatile
変更を使用するだけで済みます。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上記のコードは、二重ロック検証の形式でのシングルトンの典型的な実装であり、volatile
複数のスレッドが同時にアクセスできるシングルトンを変更するためにキーワードが使用されます。
揮発性の原理
[Java メモリ モデルとは何かと尋ねられたら、この記事を送ってください][1] で、プロセッサの実行速度を向上させるために、プロセッサとメモリの間にマルチレベル キャッシュが追加されることを紹介しました。 . 改善すること。しかし、多値キャッシュの導入により、キャッシュデータの不整合が問題となります。
ただし、volatile
変数の場合、変数が書き込まれるときvolatile
、JVM はロック プレフィックス命令をプロセッサに送信して、キャッシュ内の変数をシステム メイン メモリに書き戻します。
ただし、メモリに書き戻したとしても、他のプロセッサがキャッシュした値が古いままだと、演算処理に問題が生じるため、マルチプロセッサ環境下では、各プロセッサのキャッシュを確保するために、一貫性がある場合、実装されます。缓存一致性协议
キャッシュ整合性プロトコル: 各プロセッサは、バス上に伝播されたデータをスニッフィングすることによって、キャッシュの値が期限切れになっているかどうかをチェックします。プロセッサは、そのキャッシュ ラインに対応するメモリ アドレスが変更されたことを検出すると、現在のプロセッサのキャッシュ ラインを更新します。プロセッサがデータを変更したい場合、システム メモリからプロセッサ キャッシュにデータを再度読み取る必要があります。
したがって、変数がvolatile
変更されると、データが変更されるたびにその値がメイン メモリに強制的にフラッシュされます。他のプロセッサのキャッシュも、キャッシュ コヒーレンシ プロトコルに準拠しているため、この変数の値をメイン メモリから独自のキャッシュにロードします。これにより、volatile
同時プログラミングにおいて値が複数のキャッシュで確実に表示されるようになります。
揮発性と可視性
可視性とは、複数のスレッドが同じ変数にアクセスするときに、1 つのスレッドが変数の値を変更し、他のスレッドが変更された値をすぐに確認できることを意味します。
私たちはこれを [Java メモリ モデルとは何かと尋ねられたら、この記事を送ってください][1] で分析しました。Java メモリ モデルでは、すべての変数がメイン メモリに格納され、各スレッドが独自の作業メモリを持つことが規定されています。スレッドは、スレッドで使用される変数のメイン メモリのコピーをスレッドの作業メモリに保存します。スレッドによる変数のすべての操作は作業メモリで実行する必要があり、メイン メモリを直接読み書きすることはできません。異なるスレッドは互いの作業メモリ内の変数に直接アクセスできず、スレッド間で変数を転送するには、スレッド自身の作業メモリとメイン メモリの間でデータを同期する必要があります。したがって、スレッド 1 が変数の値を変更しても、スレッド 2 が表示されない場合があります。
上で述べたようにvolatile
、Java のキーワードにはvolatile
機能があり、キーワードによって変更された変数は変更直後にメイン メモリに同期され、キーワードによって変更された変数は使用前にメイン メモリからリセットされます。フラッシュされます。したがって、これを使用して、volatile
マルチスレッド操作中に変数の可視性を確保できます。
不安定かつ秩序ある
シーケンスとは、プログラムの実行順序がコードの順序に従って実行されることを意味します。
私たちはこれを [Java メモリ モデルについて尋ねられたら、この記事を送ってください][1] で分析しました。タイム スライスの導入に加えて、プロセッサの最適化と命令の再配置により、CPU は出力用のコードを入力することもあります。 -of-order の実行。たとえば、load->add->save
に最適化できますload->save->add
。ここに秩序的な問題が存在する可能性があります。
データの可視性を確保するだけでなくvolatile
、命令の再配置の最適化を禁止するなどの強力な機能も備えています。
通常の変数は、メソッドの実行中に代入結果が依存する場所で正しい結果が得られることを保証するだけであり、変数代入操作の順序がプログラム コード内の実行順序と一致していることは保証できません。
Volatile は命令の再配置を禁止することができ、これにより、コードのプログラムがコードの順序に厳密に従って実行されることが保証されます。これにより秩序が確保されます。変更された変数の操作はvolatile
コード シーケンスに従って厳密に実行され、load->add->save
実行シーケンスはロード、追加、保存です。
揮発性と原子性
原子性とは、操作が中断不可能であり、完全に実行される必要があり、そうでない場合はまったく実行されないことを意味します。
[Java の同時プログラミング] におけるマルチスレッドの問題で何が起こっているのでしょうか? ][3] 分析: スレッドは CPU スケジューリングの基本単位です。CPU にはタイム スライスの概念があり、さまざまなスケジューリング アルゴリズムに従ってスレッド スケジューリングを実行します。タイム スライスを取得した後にスレッドが実行を開始すると、タイム スライスが使い果たされると、スレッドは CPU を使用する権利を失います。したがって、マルチスレッドのシナリオでは、タイム スライスがスレッド間でローテーションされるため、アトミック性の問題が発生します。
前回の記事でsynchronized
紹介した際に、アトミック性を確保するためにバイトコード命令monitorenter
と を渡す必要があると述べましたがmonitorexit
、volatile
この2つの命令には関連性がありません。
したがって、volatile
原子性は保証できません。
volatile
代わりに、次の 2 つのシナリオで使用できますsynchronized
。
1. 操作の結果は変数の現在の値に依存しません。あるいは、単一のスレッドのみが変数の値を変更することを保証できます。
2. 変数は、他の状態変数の不変制約に参加する必要はありません。
上記のシナリオに加えて、アトミック性を確保するには、synchronized
や などの他のメソッドを使用する必要がありますconcurrent包
。
揮発性とアトミック性の例を見てみましょう。
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
上記のコードは比較的単純です。つまり、10 個のスレッドを作成し、それぞれ 1000 回のi++
操作を実行します。通常の状況では、プログラムの出力は 10000 になるはずですが、複数回実行した結果はすべて 10000 未満になります。実はこれがvolatile
原子性を満たせない理由なのです。
なぜこのようなことが起こるのでしょうか? それは、volatile は複数のスレッド間の可視性を保証できるからですinc
。しかし、inc++
原子性ではそれができません。
まとめと考察
volatile
キーワードとキーワードを取り上げましたsynchronized
。synchronized
原子性、順序付け、可視性が保証できることがわかりました。ただし、volatile
保証できるのは順序と可視性だけです。
それで、二重チェックされたロックによって実装されたシングルトンを見てみましょう。これはすでに使用されていますがsynchronized
、なぜまだ必要なのでしょうかvolatile
?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}