Th-03: volatile キーワード・メモリ可視性
マルチスレッドプログラムでは、スレッドごとに CPU キャッシュが存在するため、あるスレッドが変更した値が 別のスレッドからすぐに見えないことがあります。volatile キーワードはこの「メモリ可視性」の問題を解決します。 ただし、volatile だけでは解決できない問題もあるため、AtomicXxx クラスとの使い分けが重要です。
volatile とは何か
各 CPU コアは処理速度を上げるために、メインメモリの値をローカルの「キャッシュ」にコピーして使います。 そのため、スレッド A がある変数を変更しても、スレッド B は古いキャッシュの値を参照し続けることがあります。volatile を宣言したフィールドは、読み書きが必ずメインメモリを経由するため、 すべてのスレッドから最新の値が見えることが保証されます。
| キーワード / クラス | 可視性 | アトミック性 | 用途 |
|---|---|---|---|
| なし | ❌ | ❌ | シングルスレッドのみ |
volatile | ✅ | ❌(代入のみ保証) | フラグの読み書き(boolean など) |
AtomicInteger など | ✅ | ✅(複合操作も保証) | カウンターなど複合操作が必要な場面 |
synchronized | ✅ | ✅(ブロック全体) | 複数フィールドをまとめて保護する場面 |
volatile が有効な典型的な使い方:停止フラグ
別スレッドにループを止めるよう伝える「フラグ変数」は、volatile の代表的なユースケースです。 フラグの変更(代入)は1ステップで完結するため、volatile だけで十分に機能します。
private volatile boolean running = true;
// スレッド A: フラグを立てて停止を指示
running = false;
// スレッド B: volatile なのでキャッシュを見ずメインメモリを参照する
while (running) { ... } // running が false になると即座にループを抜けるサンプルコード
よくあるミス・注意点
⚠️ volatile をつけても count++ は非アトミック
volatile int count と宣言しても、count++ は「読み取り → 加算 → 書き戻し」の3ステップで実行されます。 2つのスレッドが同時に読み取ると、どちらも同じ値を読んで同じ値を書き戻すため、1回分のインクリメントが失われます。 カウンターには AtomicInteger を使ってください。
// ❌ volatile でもカウンターには使えない private volatile int count = 0; count++; // read(0) → add(1) → write(1) の3ステップ → 競合が発生 // ✅ カウンターには AtomicInteger を使う private final AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // アトミックに1加算
⚠️ volatile なしでフラグを宣言すると JIT 最適化で無限ループになる可能性がある
volatile のないフィールドは、JIT コンパイラがキャッシュから読み取るよう最適化することがあります。 その場合、別スレッドが running = false に変更しても、ループしているスレッドには反映されず、 永遠にループが終わらないことがあります。停止フラグには必ず volatile を付けてください。
テストする観点
- ✅
volatileフラグをfalseに設定すると、別スレッドのループが一定時間内に終了すること - ✅
AtomicCounterで 5スレッド × 1000回インクリメントした結果が必ず 5000 になること - ✅
VolatileCounterは複数回実行すると 5000 にならない場合があること(確率的な競合) - ✅
AtomicBoolean.compareAndSet(true, false)が1つのスレッドのみ成功し、他はfalseを返すこと