概要

マルチスレッドプログラミングで最も厄介な問題のひとつがデッドロックです。2つ以上のスレッドが互いのロック解放を待ち続け、処理が永久に停止します。発生すると自然に回復することはなく、アプリケーションの再起動が必要になります。デッドロックは4つの条件が同時に成立したときに起こり、そのうち1つを崩せば防止できます。この記事では、デッドロックの発生条件を確認したうえで、ロック取得順序の統一による予防、tryLock によるタイムアウト付き回避、ThreadMXBean によるランタイム検出の3つのアプローチを、動くコードとともに整理します。

使いどころ

複数テーブルの排他更新を行うバッチで、テーブルのロック取得順序を統一してデッドロックを防止する

外部システム連携で tryLock を使い、一定時間内にロック取得できなければタイムアウトエラーとして扱う

本番環境の監視バッチで ThreadMXBean を定期実行し、デッドロックの発生を早期に検知してアラートを出す

コード例

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

public class DeadlockDetectionDemo {

    // デッドロックのパターン(ロック逆順取得)
    static class DeadlockRisk {
        private final Object lockA = new Object();
        private final Object lockB = new Object();

        // A -> B の順
        public void task1() {
            synchronized (lockA) {
                System.out.println("Task1: lockA 取得");
                synchronized (lockB) {
                    System.out.println("Task1: lockB 取得");
                }
            }
        }

        // 修正版: 順序を A -> B に統一
        public void task2Fixed() {
            synchronized (lockA) {
                System.out.println("Task2: lockA 取得");
                synchronized (lockB) {
                    System.out.println("Task2: lockB 取得");
                }
            }
        }
    }

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

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

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

    public static void main(String[] args) throws Exception {
        checkDeadlock();

        var demo = new TimeoutLockDemo();
        var success = demo.tryBothLocks();
        System.out.println("tryLock 結果: " + success);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var(JEP 286)によるローカル変数の型推論で tryLock や TimeUnit の記述が簡潔になる。record でロック状態を表現することも可能。

Java 17
// Java 17: var で型推論を活用
var gotA = lockA.tryLock(100, TimeUnit.MILLISECONDS);
if (!gotA) return false;
try {
    var gotB = lockB.tryLock(100, TimeUnit.MILLISECONDS);
    if (!gotB) return false;
    // ...

Library Comparison

ロック順序統一(設計パターン)デッドロックを根本から防止したいとき。設計段階でロック取得順を決めておく。コードベースが大きくなると順序の管理が難しい。ドキュメント化とコードレビューでの徹底が必要。
tryLock(ReentrantLock)ロック取得に時間制限を設け、デッドロック状態でもタイムアウトで脱出したいとき。デッドロック自体を防止するのではなく、発生時のリカバリー手段。タイムアウト後のリトライ戦略を別途設計する必要がある。
ThreadMXBean本番環境でデッドロックの発生を監視・検出したいとき。運用監視ツールとの連携に向く。デッドロックの検出はできるが、自動的な解消はできない。検出後のアクション(アラート・再起動等)は別途実装が必要。

注意点

デッドロックはテスト環境では再現しにくい。タイミング依存のため、本番の高負荷時にのみ発生するケースが多い

synchronized のネストが深くなるとロック取得順序の管理が困難になる。ネストは2段以下に抑える設計を心がける

tryLock のタイムアウト値を短くしすぎると、正常な負荷時にもロック取得失敗が頻発する。業務の許容待ち時間に基づいて設定する

ThreadMXBean.findDeadlockedThreads() は ReentrantLock のデッドロックも検出できるが、カスタムロック機構のデッドロックは検出できない

FAQ

デッドロックが発生したらスレッドダンプを取るべきですか。

はい。jstack やスレッドダンプでどのスレッドがどのロックを保持・待機しているかを確認できます。ThreadMXBean.findDeadlockedThreads() でもプログラム内から検出可能です。

synchronized だけでデッドロックは起きますか。

はい。synchronized でも複数のロックを異なる順序で取得するとデッドロックが発生します。tryLock による回避策を使いたい場合は ReentrantLock に変更する必要があります。

デッドロックの4条件のうち、最も崩しやすいのはどれですか。

循環待機の防止(ロック取得順序の統一)が最も実用的です。設計段階で順序を決めておけば、コードの変更だけで防止できます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます