十五回目の投稿です。今回の内容は、Javaプログラムにおけるスレッドダンプの取得方法に関する
ものです。参画中の案件では、業務システムの運用保守を担当しており、サーバの設定変更やログ
監視など行っています。直近の業務にて、システムを構成しているJavaベースのアプリケーション
からアラートが発報され、その発報原因を調査する機会がありました。一般的なアラートの原因
調査では、当該アプリケーションが稼働するサーバのログなどの資料が必要となりますが、 Java
関連のアラートの場合は、それらに加えてスレッドダンプを取得することが多いです。本投稿では、
その際に学んだスレッドダンプの取得方法を共有し、Javaプログラムに対するスレッドダンプの
取得を行ってみます。
◆ スレッドダンプと取得対象プログラムの概要
Javaプログラムのスレッドダンプを取得する前に、関連する用語と取得対象のプログラムについて
説明します。
【関連用語】
・プロセス:OS上で実行中のプログラムのことで、例えばWebブラウザなどが該当する。
・スレッド:プロセスにおける一連の処理のことで、例えばWebブラウザにおいては、特定の
ページを取得して表示するといった一連の処理が該当する。
・スレッドダンプ:プロセスにおけるスレッドの情報を出力すること。
抽象的で分かりづらいですが、具体的な内容については参考1や参考2が分かりやすいです。Java
プログラムにおけるスレッドダンプは、該当プログラムのプロセスIDを確認し、それをスレッド
ダンプ用のユーティリティに渡すことで取得できます。
【取得対象プログラム】
スレッドダンプを取得する目的の一つとして、プログラムの意図しない動作を解析することが
挙げられます。そこで今回は、複数のスレッドが互いの終了を待ち、結果として処理が進まなく
なる(すなわちデッドロックとなる)ような次のJavaプログラム(DeadlockTest.java)を考え、
このプログラムのスレッド ダンプを取得してみます。
--
public class DeadlockTest {
public static final String STR = "resource";
public static final Double PI = 3.1415926;
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
}
private static class Thread1 extends Thread {
@Override
public void run() {
synchronized(STR) {
System.out.println("Thread1 locked the String object.");
try {
Thread.sleep(5000);
} catch (Exception e) {}
synchronized(PI) {
System.out.println("Thread1 locked the Double object.");
}
}
}
}
private static class Thread2 extends Thread {
@Override
public void run() {
synchronized(PI) {
System.out.println("Thread2 locked the Double object.");
try {
Thread.sleep(5000);
} catch (Exception e) {}
synchronized(STR) {
System.out.println("Thread2 locked the String object.");
}
}
}
}
}
--
このプログラムの実際の処理の流れは、次のとおりです。
[1] 検証用の2つのスレッドのインスタンス(それぞれt1とt2)を生成し、
それぞれのrun()メソッドが実行される
[2] t1とt2が、それぞれ次の処理を実行しようとする
・t1:STRをロックし、その状態で5秒後にPIをロックしようとする
・t2:PIをロックし、その状態で5秒後にSTRをロックしようとする
※ ロックとは、他のスレッドから参照不可となるよう制限することを表す
[3] デッドロックが発生する
Thread1クラスのrun()メソッドは、まずSTRをロックし、その状態で5秒後にPIをロックします。
一方のThread2クラスのrun()メソッドは、まずPIをロックし、その状態で5秒後にSTRをロック
します。ロックの順番が互いに逆であることがポイントで、これがデッドロックが発生しうる
原因になっています。t1がSTRをロックし、5秒待つ間にt2がPIをロックします。するとt1はPIを
ロックできなくなり、その結果として処理が進まず、t1はSTRのロック解除ができなくなっている
ということです(そのため、t2もSTRをロックできない)。
※ synchronizedブロックで囲まれた箇所は、ロック対象のオブジェクト(STRやPI)を
ロックできたスレッドのみが実行できる
なお、ロック対象のリソースの型が違うのは、スレッドダンプ時の出力を分かりやすくするため
であり、synchronizedはプリミティブ型をロック対象に指定できないため、Double型を用いて
います。 プログラムの実行とデッドロックの発生については、次節で確認します。
◆ スレッドダンプの取得
本節では、実際にスレッドダンプを取得し、その結果からデッドロックの状態を解析してみます。
【手順】
[1] 該当プログラムの実行
前節で紹介したプログラムをコンパイルし、クラスファイルを実行します。
$ javac DeadlockTest.java
$ java DeadlockTest
処理が進まず、デッドロックが発生していることが分かります。プログラムの実行を中断せず、
この状態のまま次の手順へ進みます。
[2] プロセスIDの取得
スレッドダンプを取得するために、該当のJavaプログラムのプロセスIDを取得します。
新しいシェルにて、psコマンドやjpsコマンドを実行します。なお、jps コマンドは、Java関連の
プロセスIDのみを出力します。
$ ps aux | grep java
$ jps
プログラム「DeadlockTest」のプロセスIDが「3459」であることが分かりました。
[3] スレッドダンプの取得
jstackコマンドにて、デッドロックが起こっているプログラムのスレッドダンプを取得できます。
[2]で取得したプロセスIDをjstackコマンドの引数に指定します。
$ jstack <プロセスID>
スレッドダンプが出力されたことが分かります。出力結果の詳細を確認します。
【解析】
出力結果には、次の画面キャプチャのような項目があり、「waiting to lock」や locked」、
「Found 1 deadlock」といった文言が記載されています。
この内容を見ると、二つのスレッド「Thread-0」と「Thread-1」はそれぞれ次のような状況で
あることが伺えます。
・Thread-0:21行目の処理にて、PIのロック取得待ちとSTRのロック中
・Thread-1:36行目の処理にて、STRのロック取得待ちとPIのロック中
結果としては、想定どおりのデッドロックが起こっていることが分かりました。
単純な例ですが、デッドロックの取得/解析方法について確認できました。
◆ 感想など
Javaのスレッドダンプについて学ぶことで、Javaに限らず様々なプログラムの プロセスや
スレッドについて意識するきっかけとなりました。マルチプロセス/ マルチスレッドの考え方や、
これらを監視するユーティリティなどについても 調べてみようと思います。
◆ 参考
・【参考1】https://webpia.jp/thread_process/
・【参考2】https://zenn.dev/antez/books/568dd4d86562a1/viewer/531a45
・【参考3】https://style.potepan.com/articles/33247.html
以上です。