多スレッドプログラミングでは、volatile
キーワードは変数が複数のスレッド間で可視であることを保証する重要なツールであり、命令の並べ替えを防ぐのにも役立ちます。以下では、これらの概念を詳しく説明し、volatile
の使用シナリオと制約を実際の例で説明します。
可視性
マルチスレッド環境では、1 つのスレッドが共有変数を変更しても、他のスレッドがすぐにその変更を見ることはありません。これは、スレッドが変数の値をキャッシュし、直接メインメモリから読み取らない可能性があるためです。例えば:
class SharedObject {
private boolean flag = false;
public void setFlag() {
this.flag = true;
}
public boolean getFlag() {
return this.flag;
}
}
スレッド A
が setFlag()
メソッドを呼び出して flag
を true
に設定し、スレッド B
が getFlag()
メソッドを呼び出して flag
の値をチェックするとします。volatile
を使用しない場合、スレッド B
はスレッド A
による flag
の変更を見ることができない場合があります。なぜなら、flag
の更新はスレッド A
のキャッシュにのみ存在し、メインメモリに同期されていない可能性があるからです。
volatile
が可視性の問題を解決する方法
変数を volatile
と宣言することで、この変数へのすべての変更がすべてのスレッドに対して可視であることが保証されます:
class SharedObject {
private volatile boolean flag = false;
public void setFlag() {
this.flag = true;
}
public boolean getFlag() {
return this.flag;
}
}
この例では、flag
は volatile
と宣言されており、したがって flag
の書き込み操作は常にメインメモリに即座に更新され、flag
の値を読み取るときには常に最新の値がメインメモリから直接取得されるため、変数の可視性が確保されます。
volatile
の制約
volatile
は可視性を保証できますが、操作のアトミック性は保証できません。アトミック性とは、操作が完全に成功するか、完全に失敗するかのいずれかを意味します。例えば、次のコードの count++
操作はアトミックではありません:
class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
}
count++
は実際には次の 3 つのステップを含んでいます:
count
の値を読み取る。- 値を増やす。
count
に書き戻す。
2 つのスレッドが同時に increment()
メソッドを実行すると、同じ count
の値を読み取り、それぞれがその値を増やすことがあります。その結果、count
の値が実際に増加した回数よりも少なくなる可能性があります。
アトミック性を保証する方法
操作のアトミック性を保証するには、synchronized
または AtomicInteger
クラスを使用できます:
synchronized
を使用する場合:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
synchronized
を使用すると、同時に increment()
メソッドを実行できるスレッドは 1 つだけになり、count++
の操作のアトミック性が保証されます。
AtomicInteger
を使用する場合:
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
AtomicInteger
は、incrementAndGet()
のような原子操作を提供し、安全に並行操作を行うことができます。
命令の並べ替え
命令の並べ替えは、コンパイラや CPU がパフォーマンスを最適化するためにコードの命令実行順序を調整することです。これは、マルチスレッド環境で予期しない動作が発生する可能性があります。例えば:
class Example {
private int x = 0;
private boolean flag = false;
public void method1() {
x = 1;
flag = true;
}
public void method2() {
if (flag) {
System.out.println(x);
}
}
}
volatile
を使用しない場合、コンパイラや CPU は flag = true
と x = 1
の実行順序を調整する可能性があり、method2
で flag
が true
になっているが x
がまだ更新されていない状態になる可能性があります。
volatile
が命令の並べ替えをどのように処理するか
volatile
キーワードは、volatile
変数の命令の並べ替えを防止します。キーワードを使用すると、書き込み操作は読み取り操作の前に並べ替えられず、読み取り操作は書き込み操作の後に並べ替えられないため、命令の並べ替えによる問題が回避されます:
class Example {
private volatile int x = 0;
private volatile boolean flag = false;
public void method1() {
x = 1;
flag = true;
}
public void method2() {
if (flag) {
System.out.println(x);
}
}
}
この例では、flag
と x
が両方とも volatile
と宣言されているため、method1
での flag = true
が x = 1
の前に並べ替えられることはありません。そのため、method2
で x
の最新の値を正しく読み取ることができます。
まとめ
volatile
は Java で重要なツールであり、変数がマルチスレッド環境ですべてのスレッドに対して可視であり、命令の並べ替えを防ぐことを保証します。ただし、操作のアトミック性を保証することはできません。アトミック性が必要な場面では、synchronized
またはAtomicInteger
を使用することを検討してください。