マルチスレッドのコードって、動いているように見えるんですよね。

でも並行処理のバグは、テストでは通って、本番の高負荷でだけ顔を出します。「たまにしか起きないバグ」「再現しない障害」の製造元です。今回は、知っていると設計とレビューが変わるものを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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?