volatileが提供する2つの主要な機能
Javaのvolatileキーワードは、マルチスレッド環境において共有変数の整合性を保つために、「可視性」と「順序性」という2つの重要な特性を保証します。
- 可視性の保証: あるスレッドが
volatile変数を書き換えた際、その変更が即座に主メモリ(Main Memory)に反映されます。また、他のスレッドがその変数を参照するときは、CPUキャッシュではなく必ず主メモリから最新の値を読み取ることが強制されます。 - 順序性の保証: コンパイラやCPUによる「命令の再編成(リオーダリング)」を禁止します。メモリバリア(Memory Barrier)を挿入することで、特定の命令が
volatile変数の読み書きをまたいで前後に入れ替わることを防ぎます。
低レイヤーにおける実装メカニズム
1. 可視性とキャッシュコヒーレンシ
Javaメモリモデル(JMM)において、volatileはスレッドごとのローカルな作業メモリを介さず、主メモリと直接やり取りするように定義されています。ハードウェアレベルでは、CPUのMESIプロトコルなどのキャッシュコヒーレンシプロトコルを利用します。書き込みが発生すると、他のプロセッサのキャッシュにある該当データは「無効化」され、次に読み取る際に主メモリへのアクセスが強制されます。
2. メモリバリアによる順序性の制御
JVMは、volatile変数の操作前後に特定のメモリバリア命令を挿入します。これにより、プログラムの実行順序が最適化によって変更されるのを防ぎます。
- 書き込み操作(Store): 書き込みの直前に
StoreStoreバリア、直後にStoreLoadバリアを挿入します。これにより、以前の書き込みが完了してからvolatile書き込みが行われ、その結果が即座に後続の読み込みから見えるようになります。 - 読み込み操作(Load): 読み込みの直後に
LoadLoadバリアとLoadStoreバリアを挿入します。これにより、volatile読み込みが完了する前に後続の処理が実行されるのを防ぎます。
コード例:フラグによる実行制御
volatileの最も一般的な用途は、スレッド間の状態通知フラグです。
public class TaskManager {
// volatileにより、別スレッドからの停止指示を即座に検知できる
private volatile boolean isRunning = true;
public void stopTask() {
this.isRunning = false;
}
public void process() {
System.out.println("Processing started...");
while (isRunning) {
// 重い処理のシミュレーション
}
System.out.println("Processing stopped safely.");
}
}
もしisRunningにvolatileが付与されていない場合、processメソッドを実行しているスレッドが値をキャッシュし続け、他のスレッドがstopTaskを呼び出してもループが終了しないリスクがあります。
注意点と応用パターン
原子性の欠如
volatileは可視性と順序性を保証しますが、原子性(Atomicity)は保証しません。例えば、count++のような複合操作(読み取り・修正・書き込み)は、volatileを付与してもスレッドセーフにはなりません。このようなケースではAtomicIntegerやsynchronizedを使用する必要があります。
二重チェックロッキング(DCL)での活用
シングルトンパターンなどで用いられる二重チェックロッキングでは、インスタンス変数にvolatileを付与することが不可欠です。これは、オブジェクトの生成プロセス(メモリ割り当て、コンストラクタ実行、参照代入)がリオーダリングされ、未初期化のオブジェクトが参照されてしまうのを防ぐためです。
public class DatabaseConnector {
private static volatile DatabaseConnector instance;
private DatabaseConnector() { }
public static DatabaseConnector getInstance() {
if (instance == null) {
synchronized (DatabaseConnector.class) {
if (instance == null) {
// volatileがない場合、リオーダリングにより
// 半分しか初期化されていないインスタンスが公開される可能性がある
instance = new DatabaseConnector();
}
}
}
return instance;
}
}