Java | Java 詳細・モダン文法:並行・非同期 – volatile の役割

Java Java
スポンサーリンク

volatile を一言でいうと

volatile は、
この変数の“最新の値”を、すべてのスレッドから必ず見えるようにする
ためのキーワードです。

もっと砕くと、

  • 「CPU やスレッドごとのキャッシュにこもらせない」
  • 「書き込みと読み込みの順序を、ある程度きちんと保証する」

ことで、「書いたのに別スレッドから見えない」「順番がおかしく見える」といった
“見え方のバグ”を防ぐための仕組みです。

ただし、volatile は「同時に書き換えられても安全にしてくれる魔法」ではありません。
そこを勘違いしないことが、いちばん大事なポイントです。


なぜ volatile が必要になるのか

スレッドごとに「見えている世界」がズレることがある

Java のメモリモデルでは、
各スレッドが「自分用のキャッシュ」を持っていて、
変数の値をそこに覚えておくことがあります。

その結果、こんなことが起きます。

class FlagExample {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            while (running) {
                // 何か処理
            }
            System.out.println("stopped");
        });
        t.start();

        Thread.sleep(1000);
        running = false; // 止まってほしい
    }
}
Java

直感的には「1 秒後に running = false になるから、スレッドはループを抜けて止まるはず」と思いますよね。
でも、実際には いつまでも止まらない ことがあります。

理由は、ループを回しているスレッドが、
running の値を一度読んだあと、
「ずっと true のままだろう」とキャッシュしてしまい、
メインスレッドが書き換えた false を見に行かないからです。

volatile を付けると「ちゃんと見に行く」ようになる

この runningvolatile を付けると、状況が変わります。

class FlagExample {
    private static volatile boolean running = true;
    ...
}
Java

volatile が付いた変数は、

  • 書き込むとき:必ずメインメモリに書き出される
  • 読み込むとき:必ずメインメモリから読み直される

という扱いになります。

その結果、

  • メインスレッドが running = false と書く
  • ループ側のスレッドが次に running を読むときには、その false が必ず見える

という関係が保証され、
ループはちゃんと止まるようになります。


volatile が保証してくれること

1. 「最新の値が見える」(可視性)

volatile の一番大事な役割は 可視性(visibility) の保証です。

あるスレッドが volatile 変数に書き込んだ値は、
他のスレッドから必ず「その後のどこかのタイミングで」見えるようになります。

さっきの例で言えば、

  • メインスレッドが running = false と書く
  • ループ側のスレッドが running を読むときには、
    「ずっと true のまま」ということはなく、
    いつか必ず false を見る

ということです。

volatile がないと、
「書いたのに、いつまでも古い値を見続ける」
ということが起こりえます。

2. 「前後の処理の順序」がある程度守られる(happens-before)

volatile には、順序(happens-before 関係) に関する効果もあります。

ざっくり言うと、

  • あるスレッドが volatile 変数に書き込む前に行った処理は、
    その volatile を読んだスレッドから「必ず見える」

というルールがあります。

例でイメージしてみます。

class Config {
    static int value;
    static volatile boolean initialized = false;

    static void init() {
        value = 42;           // ①
        initialized = true;   // ②(volatile 書き込み)
    }

    static void use() {
        if (initialized) {    // ③(volatile 読み込み)
            System.out.println(value); // ④
        }
    }
}
Java

ここで、

  • スレッド A が init() を呼ぶ
  • スレッド B が use() を呼ぶ

という状況を考えます。

initializedvolatile であるおかげで、

  • A が ② で initialized = true と書く前に行った ① の value = 42
  • B が ③ で initialized を読んで true を見たときには、必ず反映されている

という関係が保証されます。

つまり、initializedtrue なら、
value は必ず 42 になっている、という前提で ④ を書けるわけです。


volatile が「してくれない」こと(ここが超重要)

1. 複数ステップの操作を「まとめて安全」にしてくれるわけではない

volatile可視性 を保証しますが、
排他制御(アトミック性) は保証しません。

例えば、こんなコードがあります。

class Counter {
    volatile int value = 0;

    void increment() {
        value++; // 読み取り → 足し算 → 書き戻し
    }
}
Java

valuevolatile を付けても、
value++ は依然として「複数ステップの操作」です。

複数スレッドが同時に increment() を呼ぶと、

  • スレッド A が value を読む(0)
  • スレッド B も value を読む(0)
  • A が 1 を書く
  • B が 1 を書く

という順番になり、
結果として「2 回インクリメントしたのに 1 しか増えていない」
ということが普通に起こります。

volatile は「最新の値が見える」ようにはしてくれますが、
「同時に書き換えられても壊れない」ようにはしてくれません。

こういうときに必要なのは synchronizedAtomicInteger などで、
volatile だけでは不十分です。

2. 複雑な状態を守るには向いていない

volatile で安全に扱えるのは、

  • 単純なフラグ(true/false)
  • 「一度セットしたら変えない」参照の公開
  • 「読み取り専用の設定が準備できたかどうか」の合図

のような、単純な状態です。

複数の変数が絡むような状態(例:xy の整合性を保ちたい)を
volatile だけで守ろうとすると、
ほぼ確実に破綻します。

そういう場合は、

  • synchronized で「このブロック全体を一つの操作として守る」
  • LockAtomic 系クラスを使う

といった、より強い仕組みが必要です。


volatile が「ちょうどいい」典型パターン

パターン1:停止フラグ

さきほどの running フラグのように、
「ループを止めるためのフラグ」は、volatile の代表的な用途です。

class Worker {
    private volatile boolean running = true;

    void stop() {
        running = false;
    }

    void run() {
        while (running) {
            // 何か処理
        }
    }
}
Java

ここでは、

  • running の書き込みは単純な代入
  • 読み込みも単純な参照
  • 「最新の値が見える」ことだけが重要

なので、volatile で十分です。

パターン2:初期化済みフラグ+読み取り専用データ

先ほどの Config の例のように、

  • あるスレッドが設定値を準備して
  • 「準備できたよ」というフラグを volatile で立て
  • 他のスレッドはそのフラグを見てから設定値を読む

というパターンも、volatile がよく効きます。

class Config {
    static String value;
    static volatile boolean initialized = false;

    static void init() {
        value = "hello";
        initialized = true;
    }

    static String get() {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }
        return value;
    }
}
Java

ここでも、

  • value は「初期化後は書き換えない」
  • initialized は「初期化完了の合図」

という前提があるからこそ、volatile で十分になります。


synchronized と volatile のざっくりした使い分け

自分にこう問いかけてみる

その共有変数を扱うときに、自分にこう聞いてみてください。

必要なのは、“最新の値が見えること”だけか?
それとも、“複数の操作をまとめて安全にしたい”のか?

前者だけなら volatile の出番です。
後者が必要なら、synchronizedAtomic 系が必要です。

  • 「止める/始めるのフラグ」「準備完了フラグ」 → volatile 向き
  • 「カウンタ」「残高」「複数フィールドの整合性」 → volatile だけでは危険

という感覚を持っておくと、判断を間違えにくくなります。


まとめ:volatile を自分の言葉で説明するなら

あなたの言葉で volatile を説明すると、こうなります。

volatile は、変数の“最新の値”がすべてのスレッドからちゃんと見えるようにするためのキーワード。
スレッドごとのキャッシュにこもらず、書き込みと読み込みの順序にも一定のルールを与えてくれるので、
『書いたのに別スレッドから見えない』『初期化したはずなのに古い値が見える』といった
“見え方のバグ”を防げる。

ただし、volatile はあくまで“可視性”のための仕組みであって、
value++ のような複数ステップの操作を安全にすることはできない。
単純なフラグや初期化済みの合図には向いているが、
複雑な状態やカウンタには synchronizedAtomic を使うべき。」

タイトルとURLをコピーしました