概要

マルチスレッドでフラグを共有して「停止指示」を伝えたい場面は、バッチやポーリング処理で頻繁に出てきます。volatile を付ければ他のスレッドから変更が見えるようになりますが、「volatile さえ付ければスレッドセーフ」という理解は危険です。volatile が保証するのはメモリ可視性だけで、count++ のような複合操作のアトミック性は保証されません。この記事では、volatile フラグによるスレッド停止制御と、volatile カウンターで実際に競合が発生する様子を示し、AtomicInteger / AtomicBoolean との使い分けを明確にします。

使いどころ

バッチ処理のワーカースレッドに volatile boolean フラグで安全な停止指示を伝える

設定リロードの完了フラグを volatile で共有し、他スレッドが最新の設定を参照できるようにする

ステータス監視スレッドが volatile 変数を通じて進捗状態を確認する

コード例

VolatileDemo.java
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());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

ラムダ式と var(JEP 286)で Thread 生成を簡潔に記述可能。record でフラグ状態を不変の値として扱うパターンも使える。

Java 17
// Java 17: ラムダ式 + var で簡潔に
var worker = new Thread(() -> {
    while (flag.isRunning()) {
        // 処理
    }
}, "worker");

Library Comparison

volatileフラグの読み書きなど、単一変数への単純な代入・参照でメモリ可視性だけ必要なとき。複合操作(インクリメント等)のアトミック性は保証されない。フラグ用途以外では使いどころが限定的。
AtomicInteger / AtomicBooleanカウンターやフラグの更新をロックなしでアトミックに行いたいとき。複数変数にまたがる一貫性保証はできない。2つ以上の変数を同時に更新する場合は synchronized や Lock が必要。
synchronized複数の変数にまたがる一貫性保証や、条件判定と更新を一括で行いたいとき。ロック競合による性能低下が起きやすい。単一変数のフラグ制御には過剰な場合がある。

注意点

volatile int count に対する count++ は読み取り・加算・書き戻しの3ステップ。volatile はステップ間の割り込みを防がないため競合する

volatile はメモリ可視性だけを保証し、アトミック性は保証しない。複合操作には synchronized か Atomic 系クラスを使う

volatile の効果を過信して synchronized を省略すると、テスト環境では通るが本番の高負荷時にだけ発覚するバグになりやすい

JIT コンパイラが volatile なしのフィールドをレジスタにキャッシュすると、他スレッドの変更がいつまでも見えなくなる場合がある

FAQ

volatile を付ければ synchronized は不要ですか。

単純なフラグの読み書きなら volatile で足りますが、count++ のような複合操作には synchronized か AtomicInteger が必要です。volatile はアトミック性を保証しません。

AtomicBoolean と volatile boolean の違いは何ですか。

どちらもメモリ可視性を保証しますが、AtomicBoolean は compareAndSet などのアトミック操作メソッドを持ちます。単純な set/get だけなら volatile boolean で十分です。

volatile がないと本当に値の変更が見えないのですか。

JIT コンパイラがフィールドをレジスタにキャッシュするため、volatile なしだと変更が見えないケースは実際に起こります。ただし発生条件がJVM実装依存のため、テストで再現しにくいのが厄介です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます