Th-08: デッドロック・ライブロック・スターベーション
マルチスレッドプログラムで最も厄介な問題の一つがデッドロックです。 発生条件の4つを理解し、tryLock() によるタイムアウト回避とThreadMXBean による検出方法を学びましょう。
デッドロックとは
デッドロックとは、複数のスレッドが互いに相手のロック解放を待ち続け、 どのスレッドも処理を進められなくなる状態です。 デッドロックが発生すると、プログラムは応答しなくなり、手動で再起動するまで復帰できません。
デッドロックの4条件(コフマン条件)
以下の4つの条件がすべて同時に成立するとデッドロックが発生します。1つでも条件を崩せばデッドロックを防止できます。
| 条件 | 説明 | 対策 |
|---|---|---|
| 相互排除 | リソースは一度に1スレッドのみが使用できる | 読み取り専用なら ReadWriteLock の読み取りロックを使う |
| 保持と待機 | ロックを保持したまま別のロックを待ち続ける | 全ロックを一度に取得する、または取れなければ解放してリトライ |
| 非横取り | 他スレッドが保持するロックを強制取得できない | tryLock(timeout) でタイムアウトを設定する |
| 循環待機 | スレッドA→B→C→Aのように循環して待機する | ロック取得順序を全スレッドで統一する(最も有効) |
ライブロック・スターベーションとの違い
| 問題 | 特徴 | 対策 |
|---|---|---|
| デッドロック | スレッドが完全に停止して動かない | ロック順序の統一、tryLock |
| ライブロック | スレッドは動いているが進捗がない(譲り合いが永続) | ランダムな待機時間を加えてリトライのタイミングをずらす |
| スターベーション | 特定スレッドが常に後回しにされてリソースを得られない | new ReentrantLock(true) で公平モードを有効にする |
サンプルコード
Java 8 では boolean 型を明示的に宣言します。tryLock にタイムアウトを付けることでデッドロックを回避できます。
よくあるミス・注意点
⚠️ ロック取得順序がスレッドによって異なるとデッドロックが発生する
最も多い原因です。スレッド1が「lockA → lockB」の順に取得し、 スレッド2が「lockB → lockA」の順に取得すると循環待機が発生します。 全スレッドで必ず同じ順序でロックを取得するよう設計してください。
// ❌ スレッドによって取得順が違うとデッドロック // Thread1: lockA → lockB // Thread2: lockB → lockA ← 逆順! // ✅ 全スレッドで同じ順序に統一 // Thread1: lockA → lockB // Thread2: lockA → lockB ← 同じ順序
⚠️ tryLock で lockB 取得失敗時に lockA を解放しないとリソースリークになる
tryLock で最初のロックを取得した後、2つ目のロック取得に失敗した場合は 1つ目のロックを必ず解放してください。finally ブロックで unlock() を呼ぶことで確実に解放できます。
⚠️ デッドロックは ThreadMXBean で事後検出できるが、予防が最善策
ManagementFactory.getThreadMXBean().findDeadlockedThreads() はデッドロックを検出できますが、 検出時点ではすでにアプリが応答不能になっています。 定期的に実行して早期発見に使えますが、根本的な解決にはならないため 設計段階での予防が重要です。
テストする観点
- ✅ 同じ順序でロックを取得するとデッドロックが発生しないこと
- ✅
tryLock(timeout)で指定時間内にロックを取れない場合にfalseが返ること - ✅ 2つ目のロック取得失敗時に1つ目のロックが確実に解放されること
- ✅
ThreadMXBean.findDeadlockedThreads()がデッドロックなしの場合にnullを返すこと - ✅
new ReentrantLock(true)(公平モード)でスターベーションが解消されること