Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

Concurrency Utilities そのニ スレッドの拡張の巻

 
 
Tiger スタックトレース
 
 

最初はかるくいきましょう。

例外処理で何か書かなくてはいけないときにまず何を書きますか? 私は Throwable#printStackTrace メソッドです。どのクラスのどの行が例外を起こして、それがどこから呼ばれているかを知ることができます。

これはこれで非常に便利なのですが、デバッグをしているときにスタックトレースを出力したくなることありませんか? 私はあります。

イベントのようにどこから呼ばれているかわからないものや、非同期に実行しているものなどの情報を知るのにスタックトレースは有用です。

スタックとレースを出力してくれるメソッドに Thread#dumpStack があります。static なメソッドなのでこれはこれで重宝するのですが、問題はコンソールにしか出力できないことです。

例外の場合、Throwable#printStackTrace(PrintStream s) メソッドと Throwable#printStackTrace(PrintWriter s) メソッドがあるので、ストリームに書き出しができるのですが、Thread#dumpStack メソッドではできません。

さて、どうしましょう。

Tiger では Thread クラスに 2 つのメソッドが追加されました。getStackTrace メソッドと getAllStackTraces メソッドです。

厳密にいえばこれらのメソッドは Concurrency Utilities には関係ないのですが、Thread クラスということでまとめてしまいました。

さっそく、使い方を見ていきましょう。

サンプルのソース ThreadTest1.java

まずは getStackTrace メソッドからです。以下に示す部分は ThreadTest1 クラスのインナークラスの Task クラスです。単にスリープするだけのクラスですが。

    class Task implements Runnable {
        public void run() {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException ex) {
                System.out.println("Interrupted Exception.");
            }
 
            System.out.println("\nTask Thread Stack Trace.");
            StackTraceElement[] elements 
                     = Thread.currentThread().getStackTrace();
            for (int i = 0; i < elements.length; i++) {
                System.out.println(elements[i]);
            }
        }
    }

赤い字で示した部分が getStackTrace メソッドを使用している部分です。多分、この使い方が一番多いと思います。

Thread#currentThread メソッドでカレントのスレッドを取得し、そのスレッドのスタックトレースを要求します。

Thread#getStackTrace メソッドの戻り値は StackTraceElement クラスの配列です。StackTraceElement クラスは J2SE 1.4 で導入されたクラスで、Chained Exception で使用されます。例外では一足早くスタックトレースをプログラム中で扱えるようになっていたわけです。

さて、この部分の後の for ループで StackTraceElement オブジェクトを出力しています。結果はこうなります。

Task Thread Stack Trace.
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getStackTrace(Thread.java:1334)
ThreadTest1$Task.run(ThreadTest1.java:33)
java.lang.Thread.run(Thread.java:566)

よく見知った形ですね。StackTraceElement クラスについては J2SE 1.4 の Chained Exception Facility の解説を見てください。

さて、もう Thread#getAllStackTraces メソッドの方です。このメソッドの戻り値は Map インスタンスです。なんか変な感じですね。さっそく使ってみましょう。

    public ThreadTest1() {
        Thread thread = new Thread(new Task());
        thread.start();
 
        System.out.println("All Thread Stack Traces.");
        Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
  
        Iterator it = traces.entrySet().iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

getAllStackTraces メソッドは static メソッドなので、スレッドのインスタンスがなくても使用できます。

とりあえず、得られた Map オブジェクトを出力してみたました。

All Thread Stack Traces.
Thread[main,5,main]=[Ljava.lang.StackTraceElement;@affc70
Thread[Finalizer,8,system]=[Ljava.lang.StackTraceElement;@1e63e3d
Thread[Reference Handler,10,system]=[Ljava.lang.StackTraceElement;@1004901
Thread[Signal Dispatcher,10,system]=[Ljava.lang.StackTraceElement;@1b90b39
Thread[Thread-0,5,main]=[Ljava.lang.StackTraceElement;@18fe7c3

これを見るとだいたい分かりました。キーが Thread オブジェクトで、値が StackTaceElement の配列のようです。なぜ、配列か分かるかというと値の出力がシグネチャで行われており、配列の記号が使用されているからです。

つまり、[ が配列の記号になります。その後が配列の要素を示しいます。L の場合はクラスになり、その後にパッケージつきでクラス名が記されます。そして、クラス名の最後に ; が記述されます。

この出力からはスレッドが 5 つ走っており、その 1 つがメインスレッドだということです。そして、最後の Thread-0 が Task クラスを実行しているスレッドです。

そこで、メインスレッドのスタックトレースを出力してみましょう。

        System.out.println("\nCurrent Thread Stack Trace.");
        StackTraceElement[] elements 
            = (StackTraceElement[])traces.get(Thread.currentThread());
        for (int i = 0; i < elements.length; i++) {
            System.out.println(elements[i]);
        }

メインスレッドがカレントスレッドなのでキーには Thread#currentThread メソッドで取得できる Thread オブジェクトにしてあります。

あとは先ほどと同様に 1 つ 1 つ出力しただけです。出力結果はつぎのようになりました。

Current Thread Stack Trace.
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1385)
ThreadTest1.<init>(ThreadTest1.java:10)
ThreadTest1.main(ThreadTest1.java:41)

この機能を使えば、好きなときに好きなようにスレッドのスタックトレースを得ることができます。それもカレントスレッド以外のすべてのスレッドのスタックトレースもです。

デバッグに限らず、ソフトウェアの管理にも使えそうです。たとえば、定期的にスレッドのスタックトレースをログに出力し、死んでいるスレッドがないか、へんなところで処理が止まっているスレッドがないかなどを調べることができます。

なかなか使いでがありそうですね。

 
 
Tiger キャッチできない例外
 
 

さて、ここからが本題です。

その一の最後に書いたことが気にかかっていることなのです。くり返しになりますが、もう一度書いておきましょう。

問題は RuntimeException に関することです。RuntimeException は通常の Checked Exception と違って try ... catch を書く必要がありません。

try ... catch を書かないので、当たり前ですがキャッチする人はいません。シングルスレッドのプログラムでキャッチされない RuntimeException が発生すると、プログラムはスタックトレースを出力して止まってしまいます。

それでは、マルチスレッドで動いている場合はどうなるのでしょう。たとえば次のサンプルを見てください。

サンプルのソース UncaughtExceptionTest1.java

このサンプルはメインスレッドと異なるスレッドで RuntimeException を発生させます (かなり意図的ですが)。メインスレッドは無限ループになります。

public class UncaughtExceptionTest1 {
    public UncaughtExceptionTest1() {
        Thread thread = new Thread(new Task());
        thread.start();
 
        try {
            while (true) {
                Thread.sleep(1000L);
            }
        } catch (InterruptedException ex) {}
    }
 
    class Task implements Runnable {
        public void run() {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException ex) {}

            throw new RuntimeException();
        }
    }
 
    public static void main(String[] args) {
        new UncaughtExceptionTest1();
    }
}

実行してみると次のようになります。

C:\JSR166\examples>java UncaughtExceptionTest1
java.lang.RuntimeException
        at UncaughtExceptionTest1$Task.run(UncaughtExceptionTest1.java:17)
        at java.lang.Thread.run(Thread.java:566)
 

テキストだけだと分かりにくい思いますが、例外が発生してもプログラムは止まりません。ずっと動きつづけています (スリープしているだけですが)。

もしこれが javaw コマンドで実行されたり、コマンドプロンプトではないところから実行されたらどうでしょう。スタックトレースを出力するところはないので、例外が起きたかどうか分かりません。その上、アプリケーションはそのまま動きつづけてしまうのです。

このようにキャッチされない例外が発生すると、そのスレッドは止まってしまいます。しかし、他のスレッドはそのまま動きつづけます。

たとえば、スレッドプールのように複数のスレッドが複数のタスクを処理するような場合、RuntimeException が発生してもだれも気がつかないかもしれません。

それではどうすればいいのでしょうか。J2SE 1.4 までは ThreadGroup クラスを使います。

RuntimeExceptionTest1 クラスを ThreadGroup クラスを使うように変更したのが RuntimeExceptionTest1_1 クラスです。

サンプルのソース UncaughtExceptionTest1_1.java

UncaughtExceptionTest1_1 クラスでは ThreadGroup クラスの派生クラスの MyThradGroup クラスを使用します。

    class MyThreadGroup extends ThreadGroup {
        public MyThreadGroup(String name) {
            super(name);
        }
 
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("Uncaught Exception!!!");
            e.printStackTrace();
        }
    }

uncaughtException メソッドをオーバロードしています。ここがミソです。

生成するスレッドはこの MyThreadGroup クラスで管理させます。

    public UncaughtExceptionTest1_1() {
        ThreadGroup group = new MyThreadGroup("Group");
 
        // コンストラクタでスレッドグループを指定する
        Thread thread = new Thread(group, new Task(), "task");
        thread.start();
 
        try {
            while (true) {
                Thread.sleep(10000L);
            }
        } catch (InterruptedException ex) {}
    }

実行した結果は次のようになりました。

C:\JSR166\examples>java UncaughtExceptionTest1_1
Uncaught Exception!!!
java.lang.RuntimeException
        at UncaughtExceptionTest1_1$Task.run(UncaughtExceptionTest1_1.java:32)
        at java.lang.Thread.run(Thread.java:566)
 

無限ループで止まらないのは同じですが、異なるのは MyThreadGroup クラスの uncaughtException クラスで RuntimeException の例外処理を行っている点です。

プログラムの中で、キャッチできなかった RuntimeException をこのメソッドでキャッチできるのです。キャッチできればこっちのものです。ログに書いたり、落ちてしまったスレッドを起動しなおしたりという例外処理を行うことができます。

でも、わざわざこれだけのためにクラスを 1 つ作らなくてはいけないのも考えものです。また、Executors クラスが生成するスレッドプールにスレッドグループを割り当てることはできません。

そこで、後づけできるハンドラの登場です。

 

 
 
Tiger UncaughtExceptionHandler
 
 

キャッチできなかった例外をキャッチするために Tiger では、UncaughtExceptionHandler インタフェースという新しいインタフェースを導入しました。

UncaughtExceptionHandler インタフェースでは uncaughtException メソッドが定義されています。今まで ThreadGroup クラスを使用していたときと同じように記述することができます。現に ThreadGroup クラスはこのインタフェースをインプリメントするように変更されています。

Thread クラスには後から UncaughtExceptionHandler インタフェースを設定するためのメソッドが 2 つ追加されています。

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
 
    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh);

前者がデフォルトの UncaughtExceptionHandler を設定するもの、後者がスレッドのインスタンスごとに UncaughtExceptionHandler を設定するメソッドになります。

さっそく、このインタフェースを使ってサンプルを作ってみましょう。

サンプルのソース UncaughtExceptionTest2.java

まず UncaughtExceptionHandler インタフェースをインプリメントしたクラスを作っておきます。

    class UEHandler1 implements Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
            System.out.print("UEHandler1 catches the Uncaught Exception!!!");
            System.out.println(" Thread: " + t);
            e.printStackTrace();
        }
    }
 
    class UEHandler2 implements Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("UEHandler2 catches the Uncaught Exception!!!");
            System.out.println(" Thread: " + t);
            e.printStackTrace();
        }
    }

2 つ作ったのは、片方をデフォルトにしたときに区別がつけられるようにです。

UncaughtExceptionHandler インスタンスを設定するところは次のようにになります。

    public UncaughtExceptionTest2() {
        // デフォルトの UncaughtExceptionHandler の設定
        Thread.setDefaultUncaughtExceptionHandler(new UEHandler1());
 
        // thread1 はデフォルトを使用する
        Thread thread1 = new Thread(new Task(), "task1");
        thread1.start();
 
        // thread2 は UEHandler2 を使用する
        Thread thread2 = new Thread(new Task(), "task2");
        thread2.setUncaughtExceptionHandler(new UEHandler2());
        thread2.start();
    }

さて、実行するとどうなることやら。

C:\JSR166\examples>java UncaughtExceptionTest2
UEHandler1 catches the Uncaught Exception!!! Thread: Thread[task1,5,main]
java.lang.RuntimeException
        at UncaughtExceptionTest2$Task.run(UncaughtExceptionTest2.java:38)
        at java.lang.Thread.run(Thread.java:566)
UEHandler1 catches the Uncaught Exception!!! Thread: Thread[task2,5,main]
java.lang.RuntimeException
        at UncaughtExceptionTest2$Task.run(UncaughtExceptionTest2.java:38)
        at java.lang.Thread.run(Thread.java:566)
 
C:\JSR166\examples>

おかしいですね。setUncaughtExceptionHandler で設定した UEHandler2 オブジェクトが使用されていません。

デフォルトを設定しないなど試してみましたのですが、結局 setUncaughtExceptionHandler メソッドでは設定できませんでした。

正式版では直ることを期待しましょう。

 

 
 
Tiger おわりに
 
 

少なくともデフォルトの UncaughtExceptionHandler インスタンスを設定できるようになれば、Executors クラスが生成したスレッドプールでも Uncaught Exception をキャッチすることが可能になります。

ただ、これだとすべてのスレッドの例外処理を 1 つのクラスに集約してしまうので、例外処理的にはちょっとやりにくくなってしまいます。

はやく直らないかなぁ。

 

今回使用したサンプルはここからダウンロードできます。

参考

(Feb. 2004)

 
 
Go to Contents Go to Java Page