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 を付けると「ちゃんと見に行く」ようになる
この running に volatile を付けると、状況が変わります。
class FlagExample {
private static volatile boolean running = true;
...
}
Javavolatile が付いた変数は、
- 書き込むとき:必ずメインメモリに書き出される
- 読み込むとき:必ずメインメモリから読み直される
という扱いになります。
その結果、
- メインスレッドが
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()を呼ぶ
という状況を考えます。
initialized が volatile であるおかげで、
- A が ② で
initialized = trueと書く前に行った ① のvalue = 42は - B が ③ で
initializedを読んでtrueを見たときには、必ず反映されている
という関係が保証されます。
つまり、initialized が true なら、value は必ず 42 になっている、という前提で ④ を書けるわけです。
volatile が「してくれない」こと(ここが超重要)
1. 複数ステップの操作を「まとめて安全」にしてくれるわけではない
volatile は 可視性 を保証しますが、
排他制御(アトミック性) は保証しません。
例えば、こんなコードがあります。
class Counter {
volatile int value = 0;
void increment() {
value++; // 読み取り → 足し算 → 書き戻し
}
}
Javavalue に volatile を付けても、value++ は依然として「複数ステップの操作」です。
複数スレッドが同時に increment() を呼ぶと、
- スレッド A が
valueを読む(0) - スレッド B も
valueを読む(0) - A が 1 を書く
- B が 1 を書く
という順番になり、
結果として「2 回インクリメントしたのに 1 しか増えていない」
ということが普通に起こります。
volatile は「最新の値が見える」ようにはしてくれますが、
「同時に書き換えられても壊れない」ようにはしてくれません。
こういうときに必要なのは synchronized や AtomicInteger などで、volatile だけでは不十分です。
2. 複雑な状態を守るには向いていない
volatile で安全に扱えるのは、
- 単純なフラグ(true/false)
- 「一度セットしたら変えない」参照の公開
- 「読み取り専用の設定が準備できたかどうか」の合図
のような、単純な状態です。
複数の変数が絡むような状態(例:x と y の整合性を保ちたい)をvolatile だけで守ろうとすると、
ほぼ確実に破綻します。
そういう場合は、
synchronizedで「このブロック全体を一つの操作として守る」LockやAtomic系クラスを使う
といった、より強い仕組みが必要です。
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 の出番です。
後者が必要なら、synchronized や Atomic 系が必要です。
- 「止める/始めるのフラグ」「準備完了フラグ」 →
volatile向き - 「カウンタ」「残高」「複数フィールドの整合性」 →
volatileだけでは危険
という感覚を持っておくと、判断を間違えにくくなります。
まとめ:volatile を自分の言葉で説明するなら
あなたの言葉で volatile を説明すると、こうなります。
「volatile は、変数の“最新の値”がすべてのスレッドからちゃんと見えるようにするためのキーワード。
スレッドごとのキャッシュにこもらず、書き込みと読み込みの順序にも一定のルールを与えてくれるので、
『書いたのに別スレッドから見えない』『初期化したはずなのに古い値が見える』といった
“見え方のバグ”を防げる。
ただし、volatile はあくまで“可視性”のための仕組みであって、value++ のような複数ステップの操作を安全にすることはできない。
単純なフラグや初期化済みの合図には向いているが、
複雑な状態やカウンタには synchronized や Atomic を使うべき。」
