読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Threadはinterruptすれば停止するというものではないのだよ(2)

Java

以下の続き。

これを書いていたときに、「他に何かあったような‥」と思いながら、なかなか思い出せなかったので放置していたのだが、唐突に思い出したので。
今回示すどちらの例も、大雑把に言えば前回の「そもそも停止できない」に分類されるもの。

(注)コードを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つのスレッドで実行された場合に、デッドロックが発生する可能性がある、というもの。