java-recipes

ホーム マルチスレッド › Th-08

Th-08: デッドロック・ライブロック・スターベーション

マルチスレッドプログラムで最も厄介な問題の一つがデッドロックです。 発生条件の4つを理解し、tryLock() によるタイムアウト回避とThreadMXBean による検出方法を学びましょう。

デッドロックとは

デッドロックとは、複数のスレッドが互いに相手のロック解放を待ち続け、 どのスレッドも処理を進められなくなる状態です。 デッドロックが発生すると、プログラムは応答しなくなり、手動で再起動するまで復帰できません。

デッドロックの4条件(コフマン条件)

以下の4つの条件がすべて同時に成立するとデッドロックが発生します。1つでも条件を崩せばデッドロックを防止できます。

条件説明対策
相互排除リソースは一度に1スレッドのみが使用できる読み取り専用なら ReadWriteLock の読み取りロックを使う
保持と待機ロックを保持したまま別のロックを待ち続ける全ロックを一度に取得する、または取れなければ解放してリトライ
非横取り他スレッドが保持するロックを強制取得できないtryLock(timeout) でタイムアウトを設定する
循環待機スレッドA→B→C→Aのように循環して待機するロック取得順序を全スレッドで統一する(最も有効)

ライブロック・スターベーションとの違い

問題特徴対策
デッドロックスレッドが完全に停止して動かないロック順序の統一、tryLock
ライブロックスレッドは動いているが進捗がない(譲り合いが永続)ランダムな待機時間を加えてリトライのタイミングをずらす
スターベーション特定スレッドが常に後回しにされてリソースを得られないnew ReentrantLock(true) で公平モードを有効にする

サンプルコード

Sample.java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockSample {

    // デッドロックのデモ(実際には起こさず、条件を示す)
    static class DeadlockDemo {
        private final Object lockA = new Object();
        private final Object lockB = new Object();

        // スレッド1: A → B の順にロック
        public void thread1Task() {
            synchronized (lockA) {
                System.out.println("Thread1: lockA 取得");
                try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                synchronized (lockB) { // Thread2 が lockB を保持しているとデッドロック
                    System.out.println("Thread1: lockB 取得");
                }
            }
        }

        // スレッド2: B → A の順にロック(❌ 逆順 → デッドロック発生)
        public void thread2TaskBad() {
            synchronized (lockB) {
                System.out.println("Thread2: lockB 取得");
                try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                synchronized (lockA) { // Thread1 が lockA を保持しているとデッドロック
                    System.out.println("Thread2: lockA 取得");
                }
            }
        }

        // ✅ 対策: 常に同じ順番でロックを取得(A → B の統一)
        public void thread2TaskGood() {
            synchronized (lockA) { // lockA を先に取得(順序統一)
                System.out.println("Thread2 (fixed): lockA 取得");
                synchronized (lockB) {
                    System.out.println("Thread2 (fixed): lockB 取得");
                }
            }
        }
    }

    // ✅ tryLock でタイムアウト付きデッドロック回避
    static class TimeoutLockDemo {
        private final Lock lockA = new ReentrantLock();
        private final Lock lockB = new ReentrantLock();

        public boolean tryTask() throws InterruptedException {
            boolean gotA = lockA.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS);
            if (!gotA) return false;
            try {
                boolean gotB = lockB.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS);
                if (!gotB) return false;
                try {
                    System.out.println("両ロック取得成功");
                    return true;
                } finally {
                    lockB.unlock();
                }
            } finally {
                lockA.unlock();
            }
        }
    }

    // ThreadMXBean でデッドロック検出
    static void checkDeadlock() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] deadlockedIds = bean.findDeadlockedThreads();
        if (deadlockedIds == null) {
            System.out.println("デッドロックなし");
        } else {
            System.out.println("デッドロック検出! スレッド数: " + deadlockedIds.length);
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== デッドロック検出(ThreadMXBean)===");
        checkDeadlock(); // デッドロックなし

        System.out.println("\n=== tryLock によるデッドロック回避 ===");
        TimeoutLockDemo demo = new TimeoutLockDemo();
        boolean success = demo.tryTask();
        System.out.println("タスク成功: " + success);

        System.out.println("\n=== デッドロックの条件(説明) ===");
        System.out.println("1. 相互排除: リソースは一度に1スレッドのみが使用できる");
        System.out.println("2. 保持と待機: リソースを保持しながら別のリソースを待つ");
        System.out.println("3. 非横取り: 他スレッドのリソースを強制取得できない");
        System.out.println("4. 循環待機: スレッドが循環状に待機している");
        System.out.println("→ 対策: ロック取得順序を統一 / tryLock でタイムアウト設定");
    }
}

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)(公平モード)でスターベーションが解消されること

GitHub でソースコードを見る →