Threadはinterruptすれば停止するというものではないのだよ(2)
以下の続き。
これを書いていたときに、「他に何かあったような‥」と思いながら、なかなか思い出せなかったので放置していたのだが、唐突に思い出したので。
今回示すどちらの例も、大雑把に言えば前回の「そもそも停止できない」に分類されるもの。
(注)コードをJava 5.0ベースで書いてしまったが、Java 1.4.2で使えないのは、「Thread.currentThread().getId()」と「System.out.printf」くらい。
オブジェクトのロック取得待ち - synchronizedブロック
import java.util.Date; public class Main1 { /** * 一つ目のロックを取得した後で5秒スリープし、 * その後、二つ目のロックを取得し、 * interruptされるまでスリープし続ける。 */ private static class DoubleLockThread extends Thread { private Object lock1; private Object lock2; public DoubleLockThread(String name, Object lock1, Object lock2) { super(name); this.lock1 = lock1; this.lock2 = lock2; } public void run() { log("start run()"); try { log("try to get lock " + lock1); synchronized (lock1) { log("success to get lock " + lock1); Thread.sleep(5000); log("try to get lock " + lock2); synchronized (lock2) { log("success to get lock " + lock2); while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { log("sleep(1000) interrupted"); break; } } log("going to release lock " + lock2); } log("released lock " + lock2); log("going to release lock " + lock1); } log("released lock " + lock1); } catch (InterruptedException e) { log("sleep(5000) interrupted"); e.printStackTrace(System.out); } log("end run()"); } } /** * @param args */ public static void main(String[] args) { Object lock1 = new Object() { public String toString() { return "LOCK-1"; } }; Object lock2 = new Object() { public String toString() { return "LOCK-2"; } }; Thread th1 = new DoubleLockThread("TH-1", lock1, lock2); Thread th2 = new DoubleLockThread("TH-2", lock2, lock1); th1.start(); th2.start(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } log("try to interrupt both th-1 and th-2"); th1.interrupt(); th2.interrupt(); log("interrupted both th-1 and th-2"); } private static void log(String msg) { System.out.printf("%s [%2d][%-8s] %s\n", new Date(), Thread.currentThread().getId(), Thread.currentThread().getName(), msg); } }
2つの DoubleLockThread に指定するロックの順番を互いに逆にしているところがポイント。これはデッドロックの典型的な例である。
これを実行すると、次のようになる。
Tue Nov 17 20:57:00 JST 2009 [ 7][TH-1 ] start run() Tue Nov 17 20:57:00 JST 2009 [ 8][TH-2 ] start run() Tue Nov 17 20:57:00 JST 2009 [ 7][TH-1 ] try to get lock LOCK-1 Tue Nov 17 20:57:00 JST 2009 [ 8][TH-2 ] try to get lock LOCK-2 Tue Nov 17 20:57:00 JST 2009 [ 7][TH-1 ] success to get lock LOCK-1 Tue Nov 17 20:57:00 JST 2009 [ 8][TH-2 ] success to get lock LOCK-2 Tue Nov 17 20:57:05 JST 2009 [ 7][TH-1 ] try to get lock LOCK-2 Tue Nov 17 20:57:05 JST 2009 [ 8][TH-2 ] try to get lock LOCK-1 Tue Nov 17 20:57:10 JST 2009 [ 1][main ] try to interrupt both th-1 and th-2 Tue Nov 17 20:57:10 JST 2009 [ 1][main ] interrupted both th-1 and th-2
synchronizedブロックに入ろうとしているところで待っているため、interrupt()メソッドによる介入を受け付けない。このあと、いくら待ってもプログラムは終了しない。
オブジェクトのロック取得待ち - synchronizedメソッド
import java.util.Date; public class Main2 { /** * 指定されたオブジェクトのロックを取得した後、 * interruptされるまでスリープし続ける。 */ private static class LockAndSleepThread extends Thread { private Object lock; public LockAndSleepThread(String name, Object lock) { super(name); this.lock = lock; } public void run() { log("start run()"); log("try to get lock"); synchronized (lock) { log("success to get lock"); while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { log("sleep(1000) interrupted"); break; } } log("going to release lock"); } log("released lock"); log("end run()"); } } /** * 指定されたStringBufferにスレッド名をappendして終了する。 */ private static class AppendThread extends Thread { private StringBuffer buf; public AppendThread(String name, StringBuffer buf) { super(name); this.buf = buf; } public void run() { log("try to append this.name to the buffer"); buf.append(this.getName()); // blocked here log("success to append this.name to the buffer"); } } /** * @param args */ public static void main(String[] args) { // appendを呼び出すときにsynchronizedする。 StringBuffer buf = new StringBuffer("LOCK"); Thread th1 = new LockAndSleepThread("TH-1", buf); th1.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } Thread th2 = new AppendThread("TH-2", buf); th2.start(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } log("try to interrupt th-2"); th2.interrupt(); log("interrupted th-2"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } log("try to interrupt th-1"); th1.interrupt(); log("interrupted th-1"); } private static void log(String msg) { System.out.printf("%s [%2d][%-8s] %s\n", new Date(), Thread.currentThread().getId(), Thread.currentThread().getName(), msg); } }
これも同じような例であるが、原因を分かりやすくはしてある。(StringBuffer#append()でブロックされる。)
これを実行すると、次のようになる。
Tue Nov 17 21:01:04 JST 2009 [ 7][TH-1 ] start run() Tue Nov 17 21:01:04 JST 2009 [ 7][TH-1 ] try to get lock Tue Nov 17 21:01:04 JST 2009 [ 7][TH-1 ] success to get lock Tue Nov 17 21:01:09 JST 2009 [ 8][TH-2 ] try to append this.name to the buffer Tue Nov 17 21:01:19 JST 2009 [ 1][main ] try to interrupt th-2 Tue Nov 17 21:01:19 JST 2009 [ 1][main ] interrupted th-2 Tue Nov 17 21:01:29 JST 2009 [ 1][main ] try to interrupt th-1 Tue Nov 17 21:01:29 JST 2009 [ 1][main ] interrupted th-1 Tue Nov 17 21:01:29 JST 2009 [ 7][TH-1 ] sleep(1000) interrupted Tue Nov 17 21:01:29 JST 2009 [ 7][TH-1 ] going to release lock Tue Nov 17 21:01:29 JST 2009 [ 7][TH-1 ] released lock Tue Nov 17 21:01:29 JST 2009 [ 8][TH-2 ] success to append this.name to the buffer Tue Nov 17 21:01:29 JST 2009 [ 7][TH-1 ] end run()
最終的にはプログラムは終了するが、1つ目のスレッド(TH-1)を停止するまでは2つ目のスレッド(TH-2)の処理はブロックされている。これも同様にinterrupt()メソッドの介入を受け付けない。
これのもっと分かりにくいケースは、
- オブジェクト1のsynchronizedメソッドAを呼ぶと、中でオブジェクト2のsynchronizedメソッドBを呼ぶ
- オブジェクト2のsynchronizedメソッドCを呼ぶと、中でオブジェクト1のsynchronizedメソッドDを呼ぶ
という処理が2つのスレッドで実行された場合に、デッドロックが発生する可能性がある、というもの。