java-recipes

ホーム マルチスレッド › Th-03

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 になると即座にループを抜ける

サンプルコード

Sample.java
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileSample {

    // ❌ volatile なし: スレッド B がフラグ変更を認識できない場合がある
    static class NonVolatileFlag {
        private boolean running = true; // ❌ volatile なし

        public void stop() {
            running = false;
        }

        public boolean isRunning() {
            return running;
        }
    }

    // ✅ volatile あり: 変更が即座に他スレッドから見える
    static class VolatileFlag {
        private volatile boolean running = true; // ✅

        public void stop() {
            running = false;
        }

        public boolean isRunning() {
            return running;
        }
    }

    // ✅ AtomicBoolean: volatile + アトミック操作
    static class AtomicFlag {
        private final AtomicBoolean running = new AtomicBoolean(true);

        public void stop() {
            running.set(false);
        }

        public boolean isRunning() {
            return running.get();
        }
    }

    // カウンターで volatile の限界を示す(インクリメントは非アトミック)
    static class VolatileCounter {
        private volatile int count = 0; // volatile でも count++ は非アトミック

        public void increment() {
            count++; // ❌ read → add → write の3ステップ、競合が発生
        }

        public int getCount() {
            return count;
        }
    }

    static class AtomicCounter {
        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 {
        System.out.println("=== volatile フラグのデモ ===");
        VolatileFlag flag = new VolatileFlag();
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (flag.isRunning()) {
                    count++;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
                System.out.println("ループ終了: " + count + " 回実行");
            }
        }, "worker");

        worker.start();
        Thread.sleep(100);
        flag.stop(); // volatile なので worker スレッドに即座に伝わる
        worker.join();

        System.out.println("\n=== volatile counter の限界(競合) ===");
        VolatileCounter volatileCounter = new VolatileCounter();
        AtomicCounter atomicCounter = new AtomicCounter();

        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        volatileCounter.increment();
                        atomicCounter.increment();
                    }
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.printf("volatile: 期待=%d, 実際=%d%n", 5000, volatileCounter.getCount());
        System.out.printf("atomic:   期待=%d, 実際=%d%n", 5000, atomicCounter.getCount());
    }
}

よくあるミス・注意点

⚠️ 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 を返すこと

GitHub でソースコードを見る →