1. Thread Dump とスレッド状態の分析
Thread Dump(スレッドのダンプ)は、ある時点におけるアプリケーション内のすべてのスレッドの状態のスナップショットです。いわば全スレッドの集合写真で、誰が何をしているか、どこで詰まっているか、誰を待っているかが分かります。Thread Dump は、deadlock や livelock など不可解なハングを見つけるための主力ツールです。
Thread Dump を取得するには?
ターミナル経由(jstack):
Java プロセスの PID がある場合、次を実行します:
jstack <PID>
このコマンドは、各スレッドがどの状態にあり、どのモニター(ロック)を保持しているかを示しつつ、すべてのスレッドの状態をコンソールに出力します。
IDE(IntelliJ IDEA)から:
メニュー「Run」→「Show Running List」→ プロセスを選択 →「Thread Dump」。
VisualVM または JConsole から:
プロセスを開き、「Threads」タブでスナップショットを取得します。
Thread Dump の例
ダンプの抜粋:
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
ここでは、スレッド「Thread-1」はブロック状態(BLOCKED)で、1 つのモニターを保持しているが別のモニターを待っていることが分かります。もし複数のスレッドが、あるものはリソース A を保持して B を待ち、別のものは B を保持して A を待っているなら、それは典型的なデッドロックです。
スレッドの状態
| ステータス | 説明 |
|---|---|
| RUNNABLE | スレッドが実行中、または実行可能 |
| BLOCKED | モニター(ロック)の獲得待ち |
| WAITING | notify()/notifyAll() の通知待ち(例: wait() により) |
| TIMED_WAITING | タイムアウト付きの待機(例: sleep、wait(timeout)) |
| TERMINATED | スレッドが終了した |
重要: ステータス RUNNABLE は、スレッドが今まさに実行中であることを必ずしも意味しません。実行可能であるだけで(JVM のスケジューラが直ちに実行するとは限りません)、その点に注意してください。
デッドロックの兆候は?
ダンプ内で複数のスレッドが BLOCKED 状態にあり、それぞれが同じ集合内の別スレッドが保持するモニターを待っている。
ダンプの末尾で jstack は通常次のように出力します:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
スレッドが長時間 BLOCKED や WAITING に留まっている場合は、調査すべきサインです。
2. スレッドの監視とプロファイリング
VisualVM
VisualVM は無料のユーティリティで、多くの JDK に同梱されています。プロセスに接続し、スレッドの状態を確認し、Thread Dump を取得し、CPU 負荷、アクティブなスレッドや「ハングしている」スレッドを可視化できます。
Threads タブ: 作成済みスレッド数、各状態、活動履歴が見えます。
Thread Dump: 「Thread Dump」ボタンで、jstack と同様のスナップショットを取得します。
Java Mission Control と Flight Recorder
Java Mission Control (JMC): JVM をリアルタイムで高度に分析するツール。ロック、実行時間、アロケーション、待ち時間などの調査に役立ちます。
Java Flight Recorder (JFR): スレッド、ロック、ポーズなどのイベントを収集する JVM 組み込みのプロファイラ。
例: ロックの監視
VisualVM や JMC では、次のような状況が確認できます:
- スレッド「A」はオブジェクト X でブロックされている。
- スレッド「B」はオブジェクト X を保持しているが、オブジェクト Y を待っている。
- スレッド「C」はオブジェクト Y を保持しているが、オブジェクト X を待っている。
これは典型的な循環待ち(deadlock)です。
実践的な使い方
- アプリケーションを -XX:+FlightRecorder 付きで起動(または JDK 11+ を使用)。
- JMC を開いてプロセスに接続し、記録を開始(start recording)。
- ホットスポット、長時間のロック、スレッド間の競合を分析。
3. ロギングとトレース
マルチスレッドプログラムでの「目視デバッグ」は痛みの元です。クリティカルセクション(synchronized ブロック)への入退出、共有変数の操作、スレッドの待機と通知をログに記録しましょう。誰がいつリソースを獲得・解放したかを把握できます。
ログの取り方
- 標準的な手段を使う: java.util.logging、SLF4J、Log4j。
- スレッド名をログ出力する: Thread.currentThread().getName()。
- 時刻とスレッド ID を記録する。
- ロックの獲得/解放イベントを記録する。
ロギングの例
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " が lock を取得");
// クリティカルセクション
System.out.println(Thread.currentThread().getName() + " が lock を解放");
}
スレッド名の活用
スレッドには意味のある名前を付けましょう!
Thread t = new Thread(runnable, "MyWorker-1");
ロガーを用いたトレース例
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " は作業を開始");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " は synchronized に入った");
// ...
}
logger.info(Thread.currentThread().getName() + " は作業を終了");
}
}
4. 診断のベストプラクティス
ロック範囲を最小化する
ロックの保持時間は可能な限り短くしましょう。
悪い例:
synchronized(lock) {
// 長い入出力
// 重い計算
// DB へのアクセス
// ... そして最後に共有データの処理
}
良い例:
// synchronized の外: 長い I/O や計算
synchronized(lock) {
// 共有データの処理のみ
}
スレッド名を使う
意味のあるスレッド名は、ダンプやログの分析時間を節約します。
マルチスレッドのテストを書く
JUnit + CountDownLatch を使って、競合シナリオを再現しましょう。
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // 両方のスレッドの終了を待つ
try-finally を ReentrantLock に使う
Lock lock = new ReentrantLock();
lock.lock();
try {
// クリティカルセクション
} finally {
lock.unlock();
}
こうしておけば、例外が発生してもロックの解放を忘れません。相互ブロックを避けるには、タイムアウト付きで tryLock() を使いましょう。
同期が必要な理由を文書化する
「ここは synchronized が必要、理由は…」といったコメントは、時間が経っても意図の理解に役立ちます。
5. 実践: テストプログラムでのデッドロック解析
デッドロックを含むコード例
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: lockA を取得");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: lockB を取得");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: lockB を取得");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: lockA を取得");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
デッドロックの見つけ方
- プログラムを実行します(ハングします)。
- thread dump を取得します(jstack または VisualVM で)。
- 「Thread-1」と「Thread-2」を探し、各スレッドが一方のロックを保持し他方を待っていることを確認します。
- ダンプの末尾に「Found one Java-level deadlock」のセクションが現れます。
解消方法
- 常に同じ順序でロックを獲得する。
- ReentrantLock と tryLock()(タイムアウト付き)を使用し、すべてのロックを獲得できなければ解放して再試行する。
6. マルチスレッドプログラム診断の典型的な誤り
エラー No.1: スレッドダンプを読めない。 初心者はダンプに尻込みしがちです。「奇妙なスタックトレースとステータスは何?」。実際には主要なステータスを知り、BLOCKED/WAITING を探せば分析はぐっと簡単になります。
エラー No.2: スレッド名を無視する。 意味のある名前がないと、ダンプの解析は干し草の山から針を探すようなもの。面倒がらずに名前を付けましょう!
エラー No.3: 過度に大きな synchronized ブロック。 大きなコード塊を同期すると、スレッド同士がブロックしやすくなります — ダンプで頻繁な BLOCKED として現れます。
エラー No.4: RUNNABLE と実際に動いているスレッドの取り違え。 RUNNABLE は必ずしも CPU 上で「走っている」わけではありません。誰をいつ動かすかは JVM のスケジューラが決めます。
エラー No.5: 監視ツールを使わない。 多くの人が VisualVM、JMC、Flight Recorder を知らず、println で苦労しています。ツールを使いましょう — 作業が大いに楽になります。
エラー No.6: 重要操作をログに残さない。 ログがなければ、誰がいつロックを獲得/解放したかを把握するのはほぼ不可能です。
エラー No.7: 当てずっぽうでデータレースを捕まえようとする。 データレースは常に、すぐに現れるとは限りません — CountDownLatch を使ったテストで競合を誘発し、Thread.yield() で揺さぶり、共有変数の状態を分析しましょう。
GO TO FULL VERSION