スポンサーリンク

Javaで,各スレッドの状態を可視化するツール (マルチスレッドアプリでデッドロックを検出する)


マルチスレッドなアプリケーションで,各スレッドの状態は

  • (1)普通に動作中:緑
  • (2)ロックを保持しながら動作中:オレンジ
  • (3)ロック待ちのウェイト状態:赤

などの状態を遷移する。

もし(3)が長く続いたら,デッドロックが発生しているとわかる。


こういった複数スレッドの状態を調査するツールとして,フリーの「samurai」がある。

その使い方。

(1)スレッドの同期に失敗しているプログラム

まずは,ロックを使わないプログラムの例。

hogeというフォルダの中に,次のクラスを用意:


Callee.java(2つのスレッドから共通して呼ばれるクラス)

package hoge;

class Callee{
	private static long sum;
	
	// ここのsynchronizedの有無で実行結果が全く違う

	public static void echo( String caller )
	{
		// sumを初期化し,1を1憶回足す
		sum = 0;
		for( long i = 0; i < 100000000; i ++ )
		{
			sum += 1;
		}
		System.out.println( caller + "による計算結果:" + sum );
	}
}


Caller.java(スレッドとして2つインスタンス化するクラス)

package hoge;

class Caller implements Runnable{
	
	private String name;
	
	public Caller( String name ){
		this.name = name;
	}
	
	public void run(){
		for( int i = 0; i < 10; i ++ )
		{
			// 計算を実行
			Callee.echo( name );
		}
	}
}


Main.java(スレッドを作成するメインクラス)

package hoge;

public class Main{
	public static void main( String[] args ){
		
		Caller caller1 = new Caller( "caller1" );
		Thread t1 = new Thread( caller1, "1st_thread" );
		t1.start();
		
		Caller caller2 = new Caller( "caller2" );
		Thread t2 = new Thread( caller2, "2nd_thread" );
		t2.start();
	}
}

Javaでスレッドを立てる一つの方法として,RunnableなクラスをThreadに渡してstart()をかける。


2つのCallerクラスがスレッドとして別個に動く。

2つのCallerのスレッド(t1, t2)が1つのCalleeを呼び出す。


Calleeは,呼び出されたら数字の「1億」を毎回出力するはず。

しかしここではスレッドを同期していないのでうまくいかない。


hogeの上のフォルダから

   javac hoge/*.java

でコンパイルし,

   java hoge.Main

で実行すると

caller1による計算結果:164566433
caller2による計算結果:18402613
caller1による計算結果:124639149
caller2による計算結果:131277170
caller2による計算結果:157027493
caller1による計算結果:3797677
caller2による計算結果:124834262
caller1による計算結果:127815118
caller2による計算結果:144191277
caller1による計算結果:15608907
caller2による計算結果:125138662
caller1による計算結果:12472891
caller1による計算結果:130966163
caller2による計算結果:8613450
caller2による計算結果:123556362
caller1による計算結果:12745602
caller2による計算結果:121351264
caller1による計算結果:17146756
caller1による計算結果:137233440
caller2による計算結果:147522871

こんな結果になる。

毎回,Calleeクラスにおける計算結果が1億に到達するよりも前に,t1, t2 の2スレッドの間で制御が入れ替わってしまうのだ。

そのため,sumの値が両方のスレッドから操作されておかしい値になる。

(2)正しく動くマルチスレッドアプリ

このばらつきを防ぐためには,2スレッドから共通して呼び出されているクラスのメソッドを synchronized にする。

Javaマルチスレッド・排他処理
http://www.ne.jp/asahi/hishidama/home...


修正版 Callee.java

package hoge;

class Callee{
	private static long sum;
	
	// ここのsynchronizedの有無で実行結果が全く違う
	synchronized public static void echo( String caller )
	{
		// sumを初期化し,1を1憶回足す
		sum = 0;
		for( long i = 0; i < 100000000; i ++ )
		{
			sum += 1;
		}
		System.out.println( caller + "による計算結果:" + sum );
	}
}

コンパイルし直して実行すると

caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller1による計算結果:100000000
caller2による計算結果:100000000
caller2による計算結果:100000000
caller2による計算結果:100000000

こんな感じになる。

一方のスレッドが計算している間,もう一方のスレッドは割り込みをせず,待機していてくれる。(その待機がずっと続いたらデッドロック。)

(3)samuraiの使い方

各スレッドの状態を可視化しよう。


まずは,スレッドダンプを出力する。

そのためにはプログラム実行中にコマンドプロンプト上でCtrl + Breakを押下すればよい。

Java - スレッドダンプの取り方
http://yusuke.homeip.net/diary/2006/0...

スレッドダンプは任意のタイミングで SIGQUIT シグナルを送る(Unix)、または キーボードで Ctrl+Break を押下する(Windows)ことで取得できます。

そのダンプ結果を保存するために,クラス実行時に

   java hoge.Main > out.log

のように,出力をログに書き出すようにする。


実行開始し,コマンドプロンプト上でCtrl + Breakを1秒〜2秒おきに繰り返し押下。

Mainの実行終了時には,out.logに下記のような情報が詰め込まれる。

2009-01-04 18:26:35
Full thread dump Java HotSpot(TM) Client VM (11.0-b16 mixed mode, sharing):

"DestroyJavaVM" prio=6 tid=0x00316c00 nid=0xe0 waiting on condition [0x00000000..0x0093fd4c]
   java.lang.Thread.State: RUNNABLE

"2nd_thread" prio=6 tid=0x02b45400 nid=0x12b4 waiting for monitor entry [0x02ecf000..0x02ecfb14]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at hoge.Callee.echo(Callee.java:10)
	- waiting to lock <0x26a283a0> (a java.lang.Class for hoge.Callee)
	at hoge.Caller.run(Caller.java:14)
	at java.lang.Thread.run(Unknown Source)

"1st_thread" prio=6 tid=0x02b54800 nid=0x17e4 runnable [0x02e7f000..0x02e7fb94]
   java.lang.Thread.State: RUNNABLE
	at hoge.Callee.echo(Callee.java:11)
	- locked <0x26a283a0> (a java.lang.Class for hoge.Callee)
	at hoge.Caller.run(Caller.java:14)
	at java.lang.Thread.run(Unknown Source)

"Low Memory Detector" daemon prio=6 tid=0x02b22400 nid=0x9f8 runnable [0x00000000..0x00000000]
   java.lang.Thread.State: RUNNABLE

"CompilerThread0" daemon prio=10 tid=0x02b19000 nid=0x100c waiting on condition [0x00000000..0x02d8f6c0]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" daemon prio=10 tid=0x02b17800 nid=0xa08 runnable [0x00000000..0x00000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=10 tid=0x02b16800 nid=0x910 waiting on condition [0x00000000..0x00000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=8 tid=0x02ad4000 nid=0x12a4 in Object.wait() [0x02c9f000..0x02c9fa14]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x229e0b28> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(Unknown Source)
	- locked <0x229e0b28> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(Unknown Source)
	at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)

"Reference Handler" daemon prio=10 tid=0x02acf400 nid=0x1080 in Object.wait() [0x02c4f000..0x02c4fb14]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x229e0a30> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:485)
	at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)
	- locked <0x229e0a30> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=10 tid=0x02acdc00 nid=0xdf4 runnable 

"VM Periodic Task Thread" prio=10 tid=0x02b23400 nid=0x1088 waiting on condition 

この情報をかんたんに可視化する。


下記サイトから,samurai.jar をダウンロード。

侍 - ログ , スレッドダンプ解析ツール
http://yusuke.homeip.net/samurai/ja/i...

コマンドラインから,

   java -jar samurai.jar

でこのJARを起動。

侍のウィンドウが開くので,out.logをドラッグ&ドロップ。

「スレッドダンプ」のタブで,冒頭のような画像が見られる。
真っ赤な行があったらデッドロック。


なお,out.logの内容が書き換わると,変更がリアルタイムで侍の画面上に反映される。


余談

もしCtrl + Breakが面倒なら,SendSignal.exeというツールで代替操作ができる。

Javaプロセスのスレッドダンプを取るツール(ツール検索編)
http://d.hatena.ne.jp/Jagi/20060515/1...

sendSignal
http://www.latenighthacking.com/proje...

使い方は,java.exe(VM)の該当プロセスに向けて

   SendSignal 3932

のようにプロセスIDを指定してやればよい。

なおシグナル送信対象としてjava.exe以外のプログラムを指定すると Ctrl + C(強制終了)と同効果とみなされるので注意。



また,ダンプログの読み方などは下記URLが参考になる。

スレッドダンプの読み方 その1 - VM 内部スレッドと main スレッド
http://yusuke.homeip.net/diary/2008/0...

スレッドダンプの森で覚えた 死のロックへの違和感
http://www.atmarkit.co.jp/fjava/rensa...

Thread.State
http://java.sun.com/j2se/1.5.0/ja/doc...