CodeGym /コース /JAVA 25 SELF /デッドロック: 原因、例、対策

デッドロック: 原因、例、対策

JAVA 25 SELF
レベル 53, レッスン 0
使用可能

1. デッドロックを深掘りする

デッドロック(「相互ブロック」)とは、2つ以上のスレッドが互いに相手を永遠に待ち続ける状況です。各スレッドは何らかの資源を保持し、別の資源を取得しようとしますが、その資源は隣のスレッドにすでに保持されています。結果として誰も先に進めず、プログラムは「フリーズ」します。狭い橋の上で車が向かい合ったまま動けないのと同じです。一方が後退しない限り、誰も通れません。

Java におけるデッドロックは例外(Exception)ではなく、プログラムの「ハング」です。スレッドは終了もエラー終了もしません。ただ互いを無限に待ち続けます。だからこそ厄介で、「ある条件がそろったときにだけ」現れるのです。

デッドロックが発生する4条件

  • 1. 相互排他(Mutual Exclusion)
    資源は同時に1つのスレッドにしか保持されない(例: オブジェクトモニター、ファイル)。
  • 2. 保持して待つ(Hold and Wait)
    スレッドは1つの資源を保持したまま、別の資源を取得しようとする。
  • 3. 先取り不可(No Preemption)
    資源はスレッドから「取り上げる」ことができない。解放できるのはそのスレッド自身だけ。
  • 4. 循環待ち(Circular Wait)
    閉じた連鎖があり、各スレッドが連鎖の次のスレッドが保持する資源を待っている。

デッドロックは、これらの条件がすべて同時に満たされたときにのみ起こります。 いずれか1つでも崩せば、相互ブロックは起こりません。

2. デッドロックのコード例

スキーム

  • 2つの資源がある: lock1 と lock2(ただのオブジェクト)。
  • スレッドAはまず lock1 を取り、その後 lock2 を取得しようとする。
  • スレッドBはまず lock2 を取り、その後 lock1 を取得しようとする。

例(真似しないでください!)

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // スレッド 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("スレッド1: lock1 を取得した");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("スレッド1: lock2 の取得を試みている");
                synchronized (lock2) {
                    System.out.println("スレッド1: lock2 を取得した");
                }
            }
        });

        // スレッド 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("スレッド2: lock2 を取得した");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("スレッド2: lock1 の取得を試みている");
                synchronized (lock1) {
                    System.out.println("スレッド2: lock1 を取得した");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

実行するとどうなるか?

  • スレッド1は lock1 を、スレッド2は lock2 を取得する。
  • 両者は 100 ミリ秒スリープして、最初のロックを取得し終える。
  • その後、それぞれ相手が保持している2つ目のロックを取得しようとする。
  • 互いに待ち続けて永遠に進まない — デッドロックが発生する。

コンソールには次のように表示されます:

スレッド1: lock1 を取得した
スレッド2: lock2 を取得した
スレッド1: lock2 の取得を試みている
スレッド2: lock1 の取得を試みている

なぜデッドロックが起きるのか(詳細)

  • 相互排他: 各ロックは同時に1つのスレッドだけが保持できる。
  • 保持して待つ: スレッド1は lock1 を保持しつつ lock2 を待ち、スレッド2は lock2 を保持しつつ lock1 を待つ。
  • 先取り不可: 外部からロックを「奪う」ことはできず、synchronized ブロックを抜けるしか解放できない。
  • 循環待ち: 2つのスレッド間で待ちの輪が閉じている。

3. デッドロックを避け、防止する方法

資源を常に同じ順序で取得する

最重要ルール: 複数のスレッドが複数の資源を取得する必要がある場合は、常に同じ順序で行う。例: すべてのコード箇所で、まず lock1、次に lock2。

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // 一貫した順序 "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // 両方の資源を扱う
        }
    }
}

順序を統一すれば循環待ちは起こらず、デッドロックの条件が崩れます。

タイムアウト付き tryLock の利用(ReentrantLock)

必要な資源が事前に分からない場合は、ReentrantLock と tryLock を使って、無限に待たないようにします。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // クリティカルセクション
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("lock2 を取得できなかったためロールバックします");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("lock1 を取得できなかったためロールバックします");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

利点: いったん取りやめて後で再試行できる。 欠点: コードは複雑になるが、デッドロックは避けられる。

ロックの保持時間を最小化する

ロックはできるだけ短く保持しましょう。synchronized/lock の内部では必要最小限だけを実行し、それ以外はクリティカルセクションの外に出します。

ネストしたロックを避ける

synchronized/lock の入れ子が少ないほど、相互ブロックのリスクは下がります。可能なら関連資源に対して1つのロックを使いましょう。

スレッドセーフな既存の構造を使う

標準ライブラリには既に解決策が多数あります。ConcurrentHashMap のようなコレクションや、java.util.concurrent のクラス群を使えば、デッドロックのリスクを減らし、コードも簡潔になります。

4. デッドロックの診断

Thread Dump と jstack

Thread Dump は JVM 内のすべてのスレッド状態のスナップショットです。

コンソールからの取得:

jstack <pid>

ここで <pid> は Java プロセスの識別子(jps で確認可能)です。

IDE のデバッグパネルには通常「Thread Dump」ボタンがあります。

ダンプで確認すべき点:

  • BLOCKED や WAITING のステータス。
  • waiting to lock ... や locked ... のようなメッセージ。
  • JVM のフレーズ: "Found one Java-level deadlock:" — 相互ブロックが検出されたときに出力される。

ダンプの一部サンプル:

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

これがデッドロックです。スレッド同士が互いを保持しています。

VisualVM などのツール

  • VisualVM — スレッド解析とデッドロック検出のための無料ツール。
  • Java Mission Control / Flight Recorder — 高度なモニタリングとプロファイリング。

VisualVM ではスレッドツリーや状態を確認し、相互ブロックの通知を受け取れます。

表:「デッドロックに陥らないために」

デッドロックの原因 回避方法
資源を異なる順序で取得する 常に同じ順序で資源を取得する
入れ子の synchronized ネストを最小化し、可能なら1つのロックにする
ロックの長時間保持 クリティカルセクション内の処理を減らす
複数のロックを同時に使用 tryLock のタイムアウトを使い、ロールバックできるようにする
スレッドセーフでないコレクション コンカレントコレクション(ConcurrentHashMap など)を使う

5. デッドロックに関する典型的なミス

誤り1: 資源を異なる順序で取得する。 最も多い原因は、スレッドごとにロックの取得順序が異なることです。「そのほうが速い」ように見えても、順序は統一してください。でなければ循環待ちは必至です。

誤り2: 不要な synchronized の入れ子。 余計な入れ子は相互ブロックのリスクを高め、コードも複雑になります。ロックモデルを簡素化しましょう。

誤り3: tryLock とタイムアウトを無視する。 synchronized でのロック取得はスレッドを無限に待たせます。確信がないなら、tryLock のタイムアウトとロールバックのロジックを使いましょう。

誤り4: ロック中に長い処理を行う。 ネットワークリクエスト、I/O、重い計算をロック下で行うと、問題の確率が一気に上がります。こうした処理はクリティカルセクションの外に出しましょう。

誤り5: Thread Dump を解析しない。 Thread Dump はデッドロック調査の最良の友です。jstack を使い、BLOCKED/WAITING 状態や「holding/waiting to lock」の連鎖を分析しましょう。

1
タスク
JAVA 25 SELF, レベル 53, レッスン 0
ロック未解除
ロボット間のバトン受け渡し:デッドロックの危険 🤖
ロボット間のバトン受け渡し:デッドロックの危険 🤖
1
タスク
JAVA 25 SELF, レベル 53, レッスン 0
ロック未解除
デッドロックからの救済:タイムアウト付きの賢いエンジニア 🛠️
デッドロックからの救済:タイムアウト付きの賢いエンジニア 🛠️
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION