- JAVA
- 技術 💻
Java並行処理の落とし穴8選|volatile・デッドロック
マルチスレッドのコードって、動いているように見えるんですよね。
でも並行処理のバグは、テストでは通って、本番の高負荷でだけ顔を出します。「たまにしか起きないバグ」「再現しない障害」の製造元です。今回は、知っていると設計とレビューが変わるものを8つ集めました。1つでも知らなかったら儲けものです。
① count++ は、1つの命令ではありません
すべての出発点が、これです。
count++ は、「読む → 1足す → 書き戻す」の3ステップに分解されて実行されます。
2つのスレッドが同時にこれをやると、両方が同じ値を読んで、両方が同じ結果を書き戻す。片方の加算が消えます。
10万回×2スレッドで回しても、20万にならない。しかも毎回違う数になる。エラーは出ません。黙って数が合わなくなるのが、並行バグの怖さなんです。
② スレッドセーフな部品を組み合わせても、スレッドセーフになりません
「スレッドセーフなクラスを使っているから大丈夫」——ここに罠があります。
if (!map.containsKey(key)) { // 確認した…
map.put(key, value); // …瞬間に、他スレッドが割り込める
}
containsKeyもputも、それぞれは安全です。でも「確認してから実行する」という2手の間には、他のスレッドが入り込めます。check-then-actと呼ばれる定番の穴です。
対策は、2手を1手にまとめた操作(computeIfAbsentなど)を使うこと。安全なのは「操作の単位」であって「クラス」ではないんです。
③ volatileは、排他してくれません
volatileを付ければスレッドセーフ——半分だけ正解です。
volatileが保証するのは、「あるスレッドが書いた値が、他のスレッドから見える」という可視性です。フラグの受け渡しには効きます。
でも①のcount++のような複合操作の割り込みは、まったく防ぎません。volatileを付けても数は合わないままです。
可視性と排他は別の問題。どちらの問題を解いているのかを言えることが、並行処理の第一歩です。
④ デッドロックには、成立条件が4つあります
AがXを持ってYを待ち、BがYを持ってXを待つ。お互い譲れず、永遠に固まる——デッドロックです。
これ、4つの条件がぜんぶ揃ったときだけ起きると整理されています。
- 相互排他
- 保持して待つ
- 横取り不可
- 循環待ち
ということは、どれか1つを崩せば、構造的に起きません。
実務でいちばん使うのが「循環待ち」潰しです。ロックを取る順番を、システム全体で統一する。AもBも必ずX→Yの順で取るなら、循環は生まれないんですよね。
⑤ 譲り合っても、固まります
デッドロックの逆パターンもあります。
衝突を検知したら、お互いにロックを手放して、少し待って、再挑戦する——礼儀正しい設計に見えますよね。
ところが2つのスレッドが完全に同じタイミングで譲り合い続けると、両方とも永遠に進めません。廊下で人とお見合いして、左右に避け続けるあの現象です。ライブロックと呼ばれます。
対策は、待ち時間をランダムにずらすこと。譲り方まで設計の対象なんです。
⑥ スレッドは、無料ではありません
「重い処理はスレッドを立てて並列に」——立てすぎると逆に遅くなります。
従来のスレッドは1本ごとに専用のスタックメモリ(既定で1MB前後)を確保し、切り替えのたびにCPUがコストを払います。数千本立てれば、メモリと切り替えだけで苦しくなるんですよね。
だから実務の定石は、スレッドプールで本数を制御すること。CPU中心の処理ならコア数程度、待ち時間中心なら多めに——仕事の性質で本数を決めるんです。
なお、Java 21で正式導入された仮想スレッドは、待ち時間中心の処理についてこの前提を大きく変えました。数万本単位で立てても軽い、新しい選択肢です。
⑦ 並行バグは、テストで見つかりません
ここが一番怖いところです。
並行バグはタイミング依存なので、同じテストが1万回通って、1万1回目に落ちます。開発機の低負荷では順番が揃ってしまい、本番の高負荷でだけ順番が乱れる。
つまり「テストが通ったから安全」が、並行処理では成立しないんです。
だから守り方は検出ではなく予防になります。レビューで「共有データはどれか」「その操作は何手か」を問う。①〜③の知識が、そのままレビュー観点なんです。
⑧ 最強の対策は、共有しないことです
ここまでの話を、ひっくり返します。
ロックの取り方を極めるより強いのが、そもそも共有される可変データを無くすことです。
作ったら変更できないオブジェクト(不変オブジェクト)は、何スレッドから読まれても絶対に壊れません。ロックも要りません。
各スレッドが自分のデータだけを触り、結果を最後に集める。「上手に共有する」より「共有を減らす」。現代の並行設計の主流は、こちらなんです。
番外編:知っておくと得する小ネタ
デッドロックは、自動検出できます
Javaならjstackでスレッドダンプを取ると、デッドロックを検出して「Found one Java-level deadlock」と表示してくれます。固まったらまずこれです。
「たまに」の正体は確率です
1万分の1で壊れる競合も、毎秒1000リクエストなら10秒に1回起きます。低確率×高頻度=本番だけの常連バグになるんですよね。
シングルスレッドという選択
Node.jsやRedisが1本のスレッド(Redisはコマンドの実行部分)で高速なのは、共有データの競合そのものを設計から消しているからです。⑧の極端な実践例です。
RJCでも、並行処理と向き合っています
RJCが携わる決済や金融のシステムは、「数が合わない」が絶対に許されない世界です。だから共有データの扱いは、設計レビューの定番論点なんですよね。
社内のJava探求グループでも、こういう仕組みの話を掘り下げています。Techチームで共有しながら開発しています。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ①と②かなと思います。「その操作は何手か」を数える癖がつくと、レビューの目が変わります。
Q. synchronizedを付ければ安全ですか?
A. 正しく囲えば安全ですが、囲う範囲を間違えると②の穴が残ります。「何を1手にしたいのか」から考えるのが確実なんですよね。
Q. 並行処理は、どう学ぶのがいいですか?
A. ①のカウンタを実際に壊してみるのが最短です。手元で数が合わない体験をすると、一生忘れないんです。
Q. 未経験からでも、分かるようになりますか?
A. なります。並行処理は「順番が入れ替わったら何が起きるか」の想像力なので、1つずつ図に描けば必ず追えます。
「動いている」と「正しい」の間に、タイミングがあります
今日の8つに共通していたのは、これでした。
並行処理のコードは、運が良い順番で動いている間は正しく見えます。順番が乱れた瞬間だけ、本性が出る。
だから「動いたからヨシ」ではなく、「どの順番で来ても大丈夫か」を問える人が、本番に強いんですよね。
みなさんが踏んだ並行バグ、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。