概要
マルチスレッドでフラグを共有して「停止指示」を伝えたい場面は、バッチやポーリング処理で頻繁に出てきます。volatile を付ければ他のスレッドから変更が見えるようになりますが、「volatile さえ付ければスレッドセーフ」という理解は危険です。volatile が保証するのはメモリ可視性だけで、count++ のような複合操作のアトミック性は保証されません。この記事では、volatile フラグによるスレッド停止制御と、volatile カウンターで実際に競合が発生する様子を示し、AtomicInteger / AtomicBoolean との使い分けを明確にします。
使いどころ
バッチ処理のワーカースレッドに volatile boolean フラグで安全な停止指示を伝える
設定リロードの完了フラグを volatile で共有し、他スレッドが最新の設定を参照できるようにする
ステータス監視スレッドが volatile 変数を通じて進捗状態を確認する
コード例
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
// volatile フラグによるスレッド停止制御
static class StopFlag {
private volatile boolean running = true;
public void stop() { running = false; }
public boolean isRunning() { return running; }
}
// volatile カウンターの競合を示す
static class VolatileCounter {
private volatile int count = 0;
public void increment() { count++; } // 非アトミック
public int getCount() { return count; }
}
// AtomicInteger で安全にカウント
static class SafeCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int getCount() { return count.get(); }
}
public static void main(String[] args) throws InterruptedException {
var flag = new StopFlag();
var worker = new Thread(() -> {
int loops = 0;
while (flag.isRunning()) {
loops++;
try { Thread.sleep(10); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("ループ終了: " + loops + " 回");
}, "worker");
worker.start();
Thread.sleep(100);
flag.stop(); // volatile なので即座に伝わる
worker.join();
var volCounter = new VolatileCounter();
var safeCounter = new SafeCounter();
var threads = new Thread[5];
for (var i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
for (var j = 0; j < 1000; j++) {
volCounter.increment();
safeCounter.increment();
}
});
threads[i].start();
}
for (var t : threads) { t.join(); }
System.out.printf("volatile: 期待=5000, 実際=%d%n", volCounter.getCount());
System.out.printf("atomic: 期待=5000, 実際=%d%n", safeCounter.getCount());
}
}Version Coverage
ラムダ式と var(JEP 286)で Thread 生成を簡潔に記述可能。record でフラグ状態を不変の値として扱うパターンも使える。
// Java 17: ラムダ式 + var で簡潔に
var worker = new Thread(() -> {
while (flag.isRunning()) {
// 処理
}
}, "worker");Library Comparison
注意点
volatile int count に対する count++ は読み取り・加算・書き戻しの3ステップ。volatile はステップ間の割り込みを防がないため競合する
volatile はメモリ可視性だけを保証し、アトミック性は保証しない。複合操作には synchronized か Atomic 系クラスを使う
volatile の効果を過信して synchronized を省略すると、テスト環境では通るが本番の高負荷時にだけ発覚するバグになりやすい
JIT コンパイラが volatile なしのフィールドをレジスタにキャッシュすると、他スレッドの変更がいつまでも見えなくなる場合がある
FAQ
単純なフラグの読み書きなら volatile で足りますが、count++ のような複合操作には synchronized か AtomicInteger が必要です。volatile はアトミック性を保証しません。
どちらもメモリ可視性を保証しますが、AtomicBoolean は compareAndSet などのアトミック操作メソッドを持ちます。単純な set/get だけなら volatile boolean で十分です。
JIT コンパイラがフィールドをレジスタにキャッシュするため、volatile なしだと変更が見えないケースは実際に起こります。ただし発生条件がJVM実装依存のため、テストで再現しにくいのが厄介です。