- JAVA
Java volatileとは?使い方とsynchronizedとの違い
volatile とは、フィールドに付ける修飾子で、「あるスレッドが書いた値が、ほかのスレッドから必ず見える」ことを保証するものです。 この性質を可視性(visibility)と呼びます。停止フラグのように「1つのスレッドが書き、ほかのスレッドが読む」変数に向いています。
一方で、volatile は count++ のような「読んで・足して・書く」処理を1手にまとめてはくれません。こちらは原子性(atomicity)と呼ばれる別の性質で、synchronized や AtomicInteger の担当です。この記事では、volatile を付けないとループが終わらないコード、volatile を付けても count++ の合計が合わないコード を実際に動かし、出力を見ながら2つの性質の違いを整理します。
確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(CPU 2コア)。掲載したコードはすべて java ファイル名.java で実行し、出力をそのまま載せています。回数や時間の数値は実行のたびに変わります。
volatileの早見表
| 項目 | 内容 |
|---|---|
| 書ける場所 | フィールド(インスタンス変数・static 変数)だけ。ローカル変数や引数には書けない |
| 書き方 | private volatile boolean running = true; |
| 保証すること | 可視性:書いた値が、その後に読むほかのスレッドから見える |
| 保証しないこと | 原子性:count++ や「確認してから更新」を1手にすること |
| ロック | 取らない。ほかのスレッドを待たせない |
| final との併用 | できない(コンパイルエラー) |
| 向く場面 | 停止フラグ、状態の公開、変更できないオブジェクトへの参照の差し替え |
| 向かない場面 | カウンタ、残数の増減、2つ以上の値をセットで変える処理 |
| 性質 | volatile | synchronized | AtomicInteger |
|---|---|---|---|
| 可視性(最新の値が見える) | あり | あり | あり |
原子性(count++ が合う) |
なし | あり | あり |
| 複数の変数をまとめて守る | できない | できる | できない |
| ほかのスレッドを待たせる | 待たせない | ロックが空くまで待たせる | 待たせない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java volatileとは?使い方とsynchronizedとの違い」
volatileを付けないとwhileループが終わらない
main スレッドが running = false にしたら、worker スレッドのループが終わる、というよくある停止フラグです。まず volatile を付けずに書きます。
public class StopFlag {
static boolean running = true; // volatile なし
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long count = 0;
while (running) { // main が false にしたら抜けるつもり
count++;
}
System.out.println("worker: ループを抜けました count=" + count);
});
worker.setDaemon(true); // 止まらなくてもプログラムは終われるようにする
worker.start();
Thread.sleep(2000);
running = false;
System.out.println("main: running を false にしました");
worker.join(3000); // 最大3秒待つ
System.out.println("main: 3秒後の worker の状態 = "
+ (worker.isAlive() ? "まだ動いている" : "終了した"));
}
}
実行結果(java StopFlag.java):
main: running を false にしました
main: 3秒後の worker の状態 = まだ動いている
main が false を書いたのに、worker は「ループを抜けました」を出さず、3秒待っても動き続けています。 同じコマンドを8回実行して、7回がこの結果でした(1回は終了しました)。worker.setDaemon(true) を外すと、プログラム自体が終わらなくなります。
volatile を付けると止まる
変えるのは1か所、フィールドの宣言に volatile を足すだけです。
public class StopFlagVolatile {
static volatile boolean running = true; // volatile あり
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long count = 0;
while (running) { // main が false にしたら抜けるつもり
count++;
}
System.out.println("worker: ループを抜けました count=" + count);
});
worker.setDaemon(true); // 止まらなくてもプログラムは終われるようにする
worker.start();
Thread.sleep(2000);
running = false;
System.out.println("main: running を false にしました");
worker.join(3000); // 最大3秒待つ
System.out.println("main: 3秒後の worker の状態 = "
+ (worker.isAlive() ? "まだ動いている" : "終了した"));
}
}
main: running を false にしました
worker: ループを抜けました count=5997311311
main: 3秒後の worker の状態 = 終了した
3回実行して3回とも終了しました。
なぜ止まらないのか
Java 言語仕様(17.3)には、まさにこの形の例が載っています。while (!this.done) Thread.sleep(1000); というループについて、「コンパイラは this.done を1回だけ読み、ループの各回でその値を使い回してよい。その場合、ほかのスレッドが値を変えてもループは終わらない」と書かれています。
つまり volatile の無いフィールドでは、実行時のコンパイラ(JIT)が「このループの中では誰も running を書き換えていないから、最初に読んだ値を使い続ければよい」と最適化してよい のです。ループに入る前に1回だけ running を確かめ、あとは読み直さずに回り続ける、という動きです。
volatile を付けると、Java 言語仕様(17.4.5)の「volatile フィールドへの書き込みは、その後に行われる同じフィールドの読み込みすべてより先に起きたものとして扱う(happens-before)」というルールが適用され、読むたびに最新の値を見にいく ことが保証されます。
-Xint では再現しない
同じ volatile なしのコードを、JIT コンパイラを使わないインタプリタだけのモード(-Xint)で動かすと、止まります。
$ java -Xint StopFlag.java
main: running を false にしました
worker: ループを抜けました count=66684833
main: 3秒後の worker の状態 = 終了した
起動オプションを変えて実行した結果をまとめます。
| 実行のしかた | 結果 | 回数 |
|---|---|---|
java StopFlag.java(volatile なし・通常) |
止まらない | 8回中7回 |
java -Xint StopFlag.java(インタプリタのみ) |
止まる | 3回中3回 |
java -XX:TieredStopAtLevel=1 StopFlag.java(軽い最適化の JIT のみ) |
止まる | 3回中3回 |
java StopFlagVolatile.java(volatile あり・通常) |
止まる | 3回中3回 |
「止まらない」現象は、最適化が十分にかかったときにだけ表に出ます。 開発中の短いテストやデバッガでの実行では止まり、長時間動かしてループが十分に温まった後で初めて止まらなくなる、という出方をします。「手元では動いたのに」となりやすい理由がここにあります。
ループに出力を1行足すと止まることがある
volatile なしのまま、ループの中に出力を1行足したコードも試しました。
public class StopFlagPrint {
static boolean running = true; // volatile なし
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long count = 0;
while (running) {
count++;
if (count % 1_000_000 == 0) {
System.out.print(""); // 空文字を出力する1行を足してみる
}
}
System.out.println("worker: ループを抜けました");
});
worker.setDaemon(true);
worker.start();
Thread.sleep(2000);
running = false;
worker.join(3000);
System.out.println("main: 3秒後の worker の状態 = "
+ (worker.isAlive() ? "まだ動いている" : "終了した"));
}
}
worker: ループを抜けました
main: 3秒後の worker の状態 = 終了した
3回実行して3回とも終了しました。ただし、これは最適化のかかり方がたまたま変わっただけで、言語仕様が保証している動きではありません。Java 言語仕様(17.3)には「Thread.sleep も Thread.yield も、同期の意味は持たない」とも明記されています。「ログを足したら直った」「sleep を入れたら直った」は直ったのではなく、見えなくなっただけと考え、フラグには volatile を付けます。
volatile intのcount++は合計がずれる
次は逆のケースです。volatile を付けたカウンタを、10スレッドが100万回ずつ足します。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileCount {
static final int THREADS = 10;
static final int LOOPS = 1_000_000;
static volatile int volatileCount = 0;
static int syncCount = 0;
static final Object LOCK = new Object();
static final AtomicInteger atomicCount = new AtomicInteger();
static double run(Runnable increment) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
return;
}
for (int j = 0; j < LOOPS; j++) {
increment.run();
}
});
threads[i].start();
}
long begin = System.nanoTime();
start.countDown();
for (Thread t : threads) {
t.join();
}
return (System.nanoTime() - begin) / 1_000_000.0;
}
public static void main(String[] args) throws InterruptedException {
System.out.printf("期待する合計: %,d%n", THREADS * LOOPS);
double t1 = run(() -> volatileCount++);
System.out.printf("volatile int の count++ 合計=%,10d 時間=%7.1f ms%n", volatileCount, t1);
double t2 = run(() -> { synchronized (LOCK) { syncCount++; } });
System.out.printf("synchronized 合計=%,10d 時間=%7.1f ms%n", syncCount, t2);
double t3 = run(() -> atomicCount.incrementAndGet());
System.out.printf("AtomicInteger 合計=%,10d 時間=%7.1f ms%n", atomicCount.get(), t3);
}
}
実行結果(java VolatileCount.java):
期待する合計: 10,000,000
volatile int の count++ 合計= 3,617,863 時間= 232.4 ms
synchronized 合計=10,000,000 時間= 529.8 ms
AtomicInteger 合計=10,000,000 時間= 203.5 ms
volatile を付けていても、1,000万のはずが 3,617,863 でした。 5回実行した結果は次のとおりです。
| 方式 | 合計(5回の結果) | 時間(5回の範囲) |
|---|---|---|
volatile int の count++ |
3,617,863 / 3,509,825 / 2,894,399 / 7,601,987 / 2,762,691 | 219〜342 ms |
| synchronized | 5回とも 10,000,000 | 530〜729 ms |
| AtomicInteger | 5回とも 10,000,000 | 186〜228 ms |
count++ は4つの命令に分かれている
count++ をコンパイルした結果を javap -c で見ると、理由がわかります。
public class Inc {
static volatile int count;
static void increment() {
count++;
}
}
$ javac Inc.java
$ javap -c -p Inc
static void increment();
Code:
0: getstatic #7 // Field count:I
3: iconst_1
4: iadd
5: putstatic #7 // Field count:I
8: return
| 命令 | 意味 | volatile が保証すること |
|---|---|---|
| getstatic | count を読む | 最新の値が読める |
| iconst_1 / iadd | 1 を足す(スレッドの手元で計算) | 関係しない |
| putstatic | count に書く | 書いた値がほかのスレッドから見える |
volatile が効くのは 「読む」と「書く」のそれぞれ1回ぶん です。読んでから書くまでの間に、ほかのスレッドが同じ値を読んで同じ結果を書き戻すことは防げません。2つのスレッドがどちらも 100 を読み、どちらも 101 を書けば、2回足したのに1しか増えません。
可視性は「最新の値が見えるか」、原子性は「途中に割り込まれないか」 で、別々の性質です。volatile が持っているのは可視性だけです。
volatile・synchronized・AtomicIntegerの比較表
| 見るところ | volatile | synchronized | AtomicInteger |
|---|---|---|---|
| 種類 | フィールドの修飾子 | メソッドの修飾子/ブロック | クラス(java.util.concurrent.atomic) |
| 可視性 | あり | あり(ロックの中で読み書きした場合) | あり |
| 原子性 | 1回の読み、1回の書きだけ | ブロックの中すべて | メソッド1回ぶん(incrementAndGet など) |
count++ の合計(今回の結果) |
ずれた(1,000万が約276万〜760万) | 合った | 合った |
| 時間(今回の結果) | 219〜342 ms | 530〜729 ms | 186〜228 ms |
| ほかのスレッドを待たせるか | 待たせない | 待たせる | 待たせない(やり直す) |
| 複数の変数をまとめて守る | できない | できる | できない |
| 書ける対象 | フィールドのみ | 任意の処理 | int の値1つ |
| 向く場面 | フラグ、状態の公開、参照の差し替え | 複数の値・複数の手順の整合性 | カウンタ、連番、残数 |
synchronized の可視性は、Java 言語仕様(17.4.5)の「モニターのアンロックは、その後に行われる同じモニターのロックすべてより先に起きたものとして扱う」というルールによるものです。書く側も読む側も同じロックを取っていること が条件で、読む側だけロックを省くと、この保証は受けられません。
どれを選ぶかの判断表
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 1つのスレッドが書き、ほかは読むだけのフラグ | volatile | 可視性だけあれば足りる |
| 複数スレッドが足したり引いたりする数値 | AtomicInteger / AtomicLong | 読む・計算・書くを1手にする必要がある |
| 「確認してから更新」する値 | AtomicInteger の compareAndSet、または synchronized | 確認と更新の間に割り込ませない |
| 2つ以上のフィールドを必ずセットで変える | synchronized / Lock、または1つのオブジェクトにまとめて volatile 参照で差し替える | 個別の volatile では組み合わせが守れない |
| 1スレッドからしか触らない | 何も付けない | 共有していなければ必要ない |
volatileを使ってよい場面の判定表
| 場面 | volatile で足りるか | 補足 |
|---|---|---|
| 停止フラグ(1スレッドが false にし、ほかが読む) | 足りる | この記事の最初の例 |
| 初期化が終わったことを知らせるフラグ | 足りる | 書くのは1回、読むのは多数 |
| 最新の測定値や状態を1つ公開する(書くのは1スレッド) | 足りる | 読む側は1つ前の値でも新しい値でも成り立つこと |
| 変更できないオブジェクトへの参照をまるごと差し替える | 足りる | 下の「設定の差し替え」の例 |
| ダブルチェックロッキングのインスタンス | 必要 | 下で説明 |
| 複数スレッドで共有する long / double | 付ける意味がある | 言語仕様上、volatile でない long / double の書き込みは32ビットずつ2回に分かれてよい |
複数スレッドからの count++ |
足りない | AtomicInteger を使う |
if (flag) { flag = false; ... } のような確認→更新 |
足りない | AtomicBoolean の compareAndSet や synchronized を使う |
| 2つのフィールドの組み合わせに意味がある | 足りない | 下の「2つの volatile」の例 |
| 配列の要素を複数スレッドで更新する | 足りない | volatile になるのは配列への参照だけ |
判定の決め手は2つです。「書くスレッドが1つか、書く値が今の値に依存しないか」「ほかの変数との組み合わせに意味がないか」。 両方に当てはまれば volatile で足ります。
設定の差し替え:参照ごと入れ替えれば混ざらない
2つの値(タイムアウト秒数とリトライ回数)をセットで更新したい場面です。まず、それぞれを volatile フィールドにした場合です。timeoutSeconds は必ず retry の10倍になるように書き込み、読む側で組み合わせが崩れた回数を数えます。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class TwoFields {
// 2つの volatile フィールドを別々に書き換える
static volatile int timeoutSeconds = 10;
static volatile int retry = 1;
public static void main(String[] args) throws InterruptedException {
AtomicInteger mixed = new AtomicInteger();
CountDownLatch done = new CountDownLatch(1);
Thread[] readers = new Thread[4];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
while (done.getCount() > 0) {
int t = timeoutSeconds;
int r = retry;
if (t / 10 != r) {
mixed.incrementAndGet();
}
}
});
readers[i].start();
}
for (int n = 1; n <= 200_000; n++) {
timeoutSeconds = n * 10;
retry = n;
}
done.countDown();
for (Thread t : readers) {
t.join();
}
System.out.println("新旧が混ざって見えた回数 = " + mixed.get());
}
}
新旧が混ざって見えた回数 = 988399
1つ1つは最新の値が見えていても、2つを読む間に書き換えが入るので、新しい timeoutSeconds と以前の retry という組み合わせが約99万回見えました。
次に、2つの値を変更できないオブジェクト(record)にまとめ、その参照を volatile にして、まるごと差し替えます。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class ConfigSwap {
// 変更できない(イミュータブルな)設定オブジェクト
record Config(int timeoutSeconds, int retry) {}
// 参照をまるごと差し替える。書くのは1スレッド、読むのは多数
static volatile Config config = new Config(10, 1);
public static void main(String[] args) throws InterruptedException {
AtomicInteger mixed = new AtomicInteger();
CountDownLatch done = new CountDownLatch(1);
Thread[] readers = new Thread[4];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
while (done.getCount() > 0) {
Config c = config; // 1回だけ読んでローカル変数に持つ
if (c.timeoutSeconds() / 10 != c.retry()) {
mixed.incrementAndGet(); // 新旧が混ざった組み合わせを数える
}
}
});
readers[i].start();
}
for (int n = 1; n <= 200_000; n++) {
config = new Config(n * 10, n); // 2つの値をセットで差し替え
}
done.countDown();
for (Thread t : readers) {
t.join();
}
System.out.println("最後の設定 = " + config);
System.out.println("新旧が混ざって見えた回数 = " + mixed.get());
}
}
最後の設定 = Config[timeoutSeconds=2000000, retry=200000]
新旧が混ざって見えた回数 = 0
読む側は参照を1回だけ読んでローカル変数に持ち、そこから2つの値を取り出す のがポイントです。参照の読み書きは1回で済むので、volatile の可視性だけで「セットで新しい」「セットで以前のまま」のどちらかになります。なお、この方法が使えるのは書くスレッドが1つのときです。複数スレッドが「今の設定を読んで一部を変えて書く」なら、AtomicReference の updateAndGet や synchronized を使います。
配列に volatile を付けても、要素は volatile にならない
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class ArrayVolatile {
static volatile int[] plain = new int[1]; // volatile なのは「配列への参照」だけ
static final AtomicIntegerArray atomic = new AtomicIntegerArray(1);
public static void main(String[] args) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
return;
}
for (int j = 0; j < 1_000_000; j++) {
plain[0]++;
atomic.incrementAndGet(0);
}
});
threads[i].start();
}
start.countDown();
for (Thread t : threads) {
t.join();
}
System.out.printf("volatile int[] の要素を ++ : %,d%n", plain[0]);
System.out.printf("AtomicIntegerArray : %,d%n", atomic.get(0));
}
}
volatile int[] の要素を ++ : 9,660,602
AtomicIntegerArray : 10,000,000
volatile int[] plain で volatile になるのは、plain という変数(配列への参照)だけ です。要素ごとに安全に更新したいときは AtomicIntegerArray を使います。
ダブルチェックロッキングでvolatileが要る理由
ダブルチェックロッキングは、「最初に使われたときに1回だけインスタンスを作り、2回目以降はロックを取らずに返す」ための書き方です。インスタンスを入れるフィールドには volatile が必要です。
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class Singleton {
static final AtomicInteger created = new AtomicInteger();
// 1. ダブルチェックロッキング(volatile が必要)
static class Settings {
private static volatile Settings instance;
final String theme;
private Settings() {
created.incrementAndGet();
theme = "dark";
}
static Settings getInstance() {
Settings local = instance; // 1回目のチェック(ロックなし)
if (local == null) {
synchronized (Settings.class) {
local = instance; // 2回目のチェック(ロックの中)
if (local == null) {
local = new Settings();
instance = local;
}
}
}
return local;
}
}
// 2. ホルダークラス方式(volatile も synchronized も書かない)
static class Registry {
private Registry() {
created.incrementAndGet();
}
private static class Holder {
static final Registry INSTANCE = new Registry();
}
static Registry getInstance() {
return Holder.INSTANCE;
}
}
public static void main(String[] args) throws InterruptedException {
Set<Object> seen = ConcurrentHashMap.newKeySet();
CountDownLatch start = new CountDownLatch(1);
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
return;
}
for (int j = 0; j < 100_000; j++) {
seen.add(Settings.getInstance());
seen.add(Registry.getInstance());
}
});
threads[i].start();
}
start.countDown();
for (Thread t : threads) {
t.join();
}
System.out.println("コンストラクタが呼ばれた回数(2クラス合計)= " + created.get());
System.out.println("見えたインスタンスの種類 = " + seen.size());
System.out.println("theme = " + Settings.getInstance().theme);
}
}
コンストラクタが呼ばれた回数(2クラス合計)= 2
見えたインスタンスの種類 = 2
theme = dark
10スレッドから合計100万回ずつ呼んでも、コンストラクタが動いたのは Settings と Registry でそれぞれ1回、見えたインスタンスも1つずつでした。
volatile が無いと何が保証されなくなるか
ポイントは 1回目のチェックがロックの外にある ことです。
| 処理 | ロック | volatile が無い場合 |
|---|---|---|
1回目の instance == null の確認 |
取っていない | 書いたスレッドとの間に happens-before の関係が無い |
new Settings() と instance = ... の代入 |
取っている | ロックの中にいるスレッド同士では正しく見える |
| 1回目のチェックで null でないと判断して return | 取っていない | 参照は見えたが、その先のフィールド(theme)の初期化が見える保証が無い |
synchronized の保証は「同じロックを取ったスレッド同士」のものでした。ロックを取らずに読む1回目のチェックは、この保証の外にいます。そのため volatile が無いと、参照だけ先に見えて、コンストラクタで代入したフィールドがまだ見えないオブジェクトを受け取る ことが、言語仕様上は許されてしまいます。Java 言語仕様(8.3.1.4)にも、volatile でないフィールドでは「コンパイラが文の順番を入れ替えることが許されている」ため、別スレッドから代入の順番が逆に見えることがある、という例が載っています。
フィールドを volatile にすると、「instance への書き込み」が「その後の instance の読み込み」より先に起きたものとして扱われ、書き込みより前に行われたコンストラクタの処理もまとめて見えるようになります。この現象は発生条件が厳しく、狙って起こせるものではないため、動かして確かめるのではなく、言語仕様の保証があるかどうかで判断する 部分です。
コードの中でいったんローカル変数 local に読んでいるのは、volatile フィールドを読む回数を1回に減らすためです。
もっと短く書ける方法:ホルダークラス
上のコードの Registry のように、static な入れ子クラスの static final フィールドに持たせる と、volatile も synchronized も書かずに「最初に使われたときに1回だけ作る」が実現できます。インスタンスを作る処理を、クラスが初めて使われるときの初期化に任せる書き方で、今回の実行でも Registry のコンストラクタが動いたのは1回だけでした。
| 方法 | 書く量 | 向く場面 |
|---|---|---|
| ダブルチェックロッキング + volatile | 多い | インスタンスフィールドを遅延初期化したい、引数を使って作りたい |
| ホルダークラス | 少ない | static なシングルトン |
static final フィールドで最初から作る |
最少 | 作るコストが小さく、遅らせる必要がない |
新しく書くなら、まずホルダークラスか static final を検討し、インスタンスごとの遅延初期化が必要なときにダブルチェックロッキングを選ぶ、という順番が書きやすいです。
待っているスレッドを止めるときは interrupt も使う
volatile のフラグは、ループが回っているスレッドには届きますが、Thread.sleep などで待っている最中のスレッドは、起きるまでフラグを見ません。待ちをすぐに解きたいときは、スレッドの割り込み(interrupt)を使います。
public class InterruptStop {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int rounds = 0;
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(200); // 待っている間でも割り込みで起きる
rounds++;
}
} catch (InterruptedException e) {
System.out.println("worker: sleep 中に割り込まれました");
}
System.out.println("worker: 終了 rounds=" + rounds);
});
worker.start();
Thread.sleep(1000);
worker.interrupt();
worker.join(3000);
System.out.println("main: worker の状態 = " + (worker.isAlive() ? "まだ動いている" : "終了した"));
}
}
worker: sleep 中に割り込まれました
worker: 終了 rounds=4
main: worker の状態 = 終了した
| 止めたい相手 | 手段 |
|---|---|
| 計算のループを回しているスレッド | volatile のフラグ、または Thread.currentThread().isInterrupted() の確認 |
| sleep や待ち合わせで待っているスレッド | thread.interrupt()(InterruptedException で起きる) |
つまずきやすい点と実際のエラー文
final と一緒には書けない
public class FinalVolatile {
static final volatile int LIMIT = 10;
}
FinalVolatile.java:2: error: illegal combination of modifiers: final and volatile
static final volatile int LIMIT = 10;
^
1 error
Java 言語仕様(8.3.1.4)で「final 変数を volatile と宣言するとコンパイルエラー」と決められています。final は書き換えないことの宣言なので、書き換えを見えるようにする volatile とは両立しません。
ローカル変数には書けない
public class Bad2 {
void run() {
volatile int local = 0;
}
}
Bad2.java:3: error: illegal start of expression
volatile int local = 0;
^
volatile はフィールド専用です。ローカル変数はそのスレッドだけのものなので、そもそも共有されません。
チェックリスト
| 確認すること | そのままにするとこうなる | こう書くとよい |
|---|---|---|
| ほかのスレッドが書き換えるフラグに volatile が付いているか | ループが終わらない(今回は8回中7回) | volatile boolean にする |
volatile の変数に ++ や += をしていないか |
合計がずれる(1,000万が約360万) | AtomicInteger / LongAdder にする |
| volatile の変数を確認してから更新していないか | 確認と更新の間にほかのスレッドが入り込む | compareAndSet、synchronized |
| 2つの volatile の組み合わせに意味を持たせていないか | 新旧が混ざる(今回は約99万回) | 1つのオブジェクトにまとめて参照を差し替える |
| 配列やコレクションの中身まで守れると考えていないか | 要素の更新がずれる | AtomicIntegerArray、ConcurrentHashMap |
| ダブルチェックロッキングのフィールドに volatile があるか | 初期化途中のオブジェクトが見える余地が残る | volatile を付ける、ホルダークラスにする |
| 「sleep やログを入れたら直った」で終わらせていないか | 条件が変わると再発する | volatile や適切な同期を入れる |
よくある質問
Q. volatile を付けると遅くなりますか? A. 読むたびに最新の値を見にいくぶん、付けないフィールドより最適化の余地は減ります。ただしロックを取らないので、今回の計測では synchronized(530〜729 ms)より volatile の count++(219〜342 ms)や AtomicInteger(186〜228 ms)のほうが短い時間でした。必要な変数にだけ付ける、という使い方で十分です。
Q. synchronized の中で読み書きしている変数にも volatile は必要ですか? A. 書く側も読む側も同じロックの中なら、必要ありません。ロックの解放と取得の間に happens-before の関係があるためです。読む側だけロックを省いている場合(ダブルチェックロッキングの1回目のチェックなど)は必要になります。
Q. volatile と AtomicBoolean はどう使い分けますか? A. 「true / false を書くだけ・読むだけ」なら volatile boolean で足ります。「まだ false のときだけ true にして、その1回だけ処理する」のように確認と更新を1手で行いたいときは、AtomicBoolean の compareAndSet を使います。
Q. long や double はなぜ特別扱いなのですか? A. Java 言語仕様(17.7)で、volatile でない long / double への1回の書き込みは、32ビットずつ2回の書き込みとして扱ってよいとされているためです。同じ節に「volatile な long と double の読み書きは常にアトミック」「参照の読み書きは常にアトミック」とも書かれています。複数スレッドで共有する long / double は、volatile か AtomicLong などにしておくと安心です。
まとめ
- volatile は「書いた値がほかのスレッドから見える」こと(可視性)を保証する、フィールド用の修飾子
- 付けないと、ほかのスレッドが false にしてもループが終わらないことがある(今回は8回中7回)。
-Xintでは再現せず、最適化がかかったときに表に出る - volatile は count++ を守らない。1,000万回のはずが約360万回になった。カウンタは AtomicInteger を使う
- 可視性は「最新が見えるか」、原子性は「割り込まれないか」。volatile は可視性だけ、synchronized と AtomicInteger は両方
- 使ってよいのは、書くスレッドが1つで、ほかの変数との組み合わせに意味がないとき。2つの値は1つのオブジェクトにまとめて参照ごと差し替える
- ダブルチェックロッキングのフィールドには volatile が必要。ロックの外で読む1回目のチェックに保証を与えるため
- 「sleep やログを足したら止まった」は保証ではない。フラグには volatile を付ける
参考・出典
- The Java Language Specification, Java SE 21 Edition:8.3.1.4 volatile Fields(docs.oracle.com)
- The Java Language Specification, Java SE 21 Edition:17.3 Sleep and Yield、17.4.5 Happens-before Order、17.5 final Field Semantics、17.7 Non-Atomic Treatment of double and long(docs.oracle.com)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java volatileとは?使い方とsynchronizedとの違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。