概要
マルチスレッドプログラミングで最も厄介な問題のひとつがデッドロックです。2つ以上のスレッドが互いのロック解放を待ち続け、処理が永久に停止します。発生すると自然に回復することはなく、アプリケーションの再起動が必要になります。デッドロックは4つの条件が同時に成立したときに起こり、そのうち1つを崩せば防止できます。この記事では、デッドロックの発生条件を確認したうえで、ロック取得順序の統一による予防、tryLock によるタイムアウト付き回避、ThreadMXBean によるランタイム検出の3つのアプローチを、動くコードとともに整理します。
使いどころ
複数テーブルの排他更新を行うバッチで、テーブルのロック取得順序を統一してデッドロックを防止する
外部システム連携で tryLock を使い、一定時間内にロック取得できなければタイムアウトエラーとして扱う
本番環境の監視バッチで ThreadMXBean を定期実行し、デッドロックの発生を早期に検知してアラートを出す
コード例
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);
}
}Version Coverage
var(JEP 286)によるローカル変数の型推論で tryLock や TimeUnit の記述が簡潔になる。record でロック状態を表現することも可能。
// 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
注意点
デッドロックはテスト環境では再現しにくい。タイミング依存のため、本番の高負荷時にのみ発生するケースが多い
synchronized のネストが深くなるとロック取得順序の管理が困難になる。ネストは2段以下に抑える設計を心がける
tryLock のタイムアウト値を短くしすぎると、正常な負荷時にもロック取得失敗が頻発する。業務の許容待ち時間に基づいて設定する
ThreadMXBean.findDeadlockedThreads() は ReentrantLock のデッドロックも検出できるが、カスタムロック機構のデッドロックは検出できない
FAQ
はい。jstack やスレッドダンプでどのスレッドがどのロックを保持・待機しているかを確認できます。ThreadMXBean.findDeadlockedThreads() でもプログラム内から検出可能です。
はい。synchronized でも複数のロックを異なる順序で取得するとデッドロックが発生します。tryLock による回避策を使いたい場合は ReentrantLock に変更する必要があります。
循環待機の防止(ロック取得順序の統一)が最も実用的です。設計段階で順序を決めておけば、コードの変更だけで防止できます。