Java

Thread.stopThread.suspend
Thread.resume、および Runtime.runFinalizersOnExit が推奨されない理由


Thread.stop が推奨されないのはなぜですか

本質的に安全ではないからです。 スレッドを停止すると、そのスレッドがロックしたすべてのモニターのロックが解除されます。 (ThreadDeath 例外がスタックまで伝わると、モニターのロックが解除される。) これらのモニターによって以前保護されていたオブジェクトが整合性のない状態になると、ほかのスレッドも、これらのオブジェクトが整合性のない状態にあると見なします。 そのようなオブジェクトは、「壊れた」オブジェクトと呼ばれます。 壊れたオブジェクトに対してスレッドが操作を実行すると、予期しない結果になる可能性があります。 この動作は、微妙で検出が困難な場合と、はっきりと通知される場合があります。 チェックされないほかの例外とは異なり、ThreadDeath は、スレッドをそのまま強制的に終了します。このため、ユーザは、プログラムが壊れる可能性を警告されることがありません。 プログラムが壊れていることは、実際に損傷を受けたしばらくあとに明らかになり、それが数時間後または数日後になることもあります。


ThreadDeath 例外をキャッチし、壊れたオブジェクトを修復することはできないのですか

理論的には、おそらく可能です。ただし、正しいマルチスレッドコードを記述するのは「非常に」複雑な作業です。 これがほとんど実行不可能な作業であることは、次の 2 つの理由によります。

  1. スレッドは、ThreadDeath 例外を「ほとんどすべての場所で」スローする。 このことを念頭に置いて、すべての同期化されたメソッドおよびブロックを詳細に調査する必要がある
  2. スレッドは、1 番目の例外のクリーンアップ中 (catch または finally 節) に、2 番目の ThreadDeath をスローすることがある。 クリーンアップは、正常に終了するまで繰り返し実行される必要がある。 この動作を確実に行うコードは非常に複雑になる
したがって、この処理は現実的ではありません。


Thread.stop(Throwable) についてはどうですか

上で説明した問題すべてに加えて、このメソッドは、対象とするスレッドの処理準備ができていないという例外 (このメソッドがなかったならスレッドがスローできないはずの、チェックされる例外を含む) を発生させるのに使用されることがあります。 たとえば、次のメソッドの動作は、Java の throw 操作と同じですが、呼び出し側のメソッドがスローする可能性のあるチェックされる例外すべてが宣言されていることを保証しようとするコンパイラをだまして失敗させます。

    static void sneakyThrow(Throwable t) {
        Thread.currentThread().stop(t);
    }


Thread.stop の代わりに何を使うべきですか

stop の代わりとしては、多くの場合、ターゲットスレッドの実行を停止するべきことを示す何らかの変数を単に変更するコードを使用します。 ターゲットスレッドは、この変数を定期的に検査し、実行を停止するべきことを変数が示している場合には、スレッドの run メソッドから通常の方法で復帰する必要があります。 これは、Java ソフトウェアのチュートリアルで常に推奨している方法です。 停止要求の即時通信を確実にするには、変数を volatile にする (または、変数へのアクセスを同期化する) 必要があります。

たとえば、アプレットに次の startstop、および run メソッドが含まれているとします。

    private Thread blinker;

    public void start() {
        blinker = new Thread(this);
        blinker.start();
    }

    public void stop() {
        blinker.stop();  // UNSAFE!
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (true) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    }
アプレットの stop および run メソッドを次のコードで置き換えれば、Thread.stop を使用しないで済みます。
    private volatile Thread blinker;

    public void stop() {
        blinker = null;
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    }


長い間 (入力などを) 待機しているスレッドをどうすれば停止できますか

その目的には、Thread.interrupt を使用します。 上と同じ「状態に基づいた」シグナル機構を使用できますが、状態変更 (前の例では blinker = null) のあとに、Thread.interrupt を呼び出して待機状態に割り込むことができます。

    public void stop() {
        Thread moribund = waiter;
        waiter = null;
        moribund.interrupt();
    }
この方法では、割り込み例外をキャッチしたメソッドがその例外を処理するように準備されていない場合に、そのメソッドでその例外をただちに再宣言することが重要です。 ここで「再スロー」ではなく「再宣言」と書いたのは、例外をいつも再スローできるとは限らないからです。 InterruptedException をキャッチしたメソッドが、この (確認済みの) 例外をスローするように宣言されていない場合は、次のような決まった書き方により「自らに再割り込みする」必要があります。
    Thread.currentThread().interrupt();
これにより、スレッドは、可能な限り早く InterruptedException を再発行できるようになります。


スレッドが Thread.interrupt に応答しないとどうなりますか

アプリケーション独自の技法が使用可能な場合もあります。 たとえば、スレッドが既知のソケット上で待機している場合は、ソケットを閉じることによりスレッドをただちに復帰させることができます。 しかし、残念ながら、汎用的に使用できる技法はありません。 待機しているスレッドが Thread.interrupt に応答しないすべての状況では、そのスレッドは Thread.stop にも応答しないことに注意してください。 そのような状況としては、意図的なサービス妨害攻撃や、thread.stop と thread.interrupt が適切に機能しない入出力操作などがあります。


Thread.suspend および Thread.resume が推奨されないのはなぜですか

Thread.suspend は、本質的にデッドロックを起こす傾向があります。 ターゲットスレッドが、中断される時点で、重要なシステムリソースを保護するモニターをロックしている場合、ターゲットスレッドが再開されるまでどのスレッドもそのリソースにアクセスできません。 このとき、ターゲットスレッドを再開するスレッドが、resume を呼び出す前にこのモニターをロックしようとすると、デッドロックが発生します。 通常、このようなデッドロックは、プロセスの「凍結」により明らかになります。


Thread.suspendThread.resume の代わりに何を使うべきですか

Thread.stop の場合と同様、賢明なアプローチは、スレッドの望ましい状態 (実行中または中断中) を示す変数を「ターゲットスレッド」にポーリングさせることです。 望ましい状態が中断中である場合、スレッドは Object.wait を使用して待機します。 スレッドが再開されたときは、ターゲットスレッドは Object.notify を使って通知を受けます。

たとえば、アプレットに次のような mousePressed イベントハンドラが含まれており、それが blinker というスレッドの状態を切り替えるとします。

    private boolean threadSuspended;

    Public void mousePressed(MouseEvent e) {
        e.consume();

        if (threadSuspended)
            blinker.resume();
        else
            blinker.suspend();  // DEADLOCK-PRONE!

        threadSuspended = !threadSuspended;
    }
上のイベントハンドラを次のコードで置き換えれば、Thread.suspend および Thread.resume を使わないで済みます。
    public synchronized void mousePressed(MouseEvent e) {
        e.consume();

        threadSuspended = !threadSuspended;

        if (!threadSuspended)
            notify();
    }
そして、次のコードを「実行ループ」に追加します。
                synchronized(this) {
                    while (threadSuspended)
                        wait();
                }
wait メソッドは、InterruptedException をスローするので、このメソッドを try ... catch 節の内部に置く必要があります。 このメソッドを sleep と同じ節に入れると効果的です。 チェックは sleep の前ではなくあとに行われるので、スレッドが「再開」されるとただちにウィンドウが再描画されます。 このように記述した run メソッドは、次のようになります。
    public void run() {
        while (true) {
            try{
                Thread.currentThread().sleep(interval);

                synchronized(this) {
                    while (threadSuspended)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }
mousePressed メソッドの notify、および run メソッドの wait は、synchronized ブロックの内部にあることに注目してください。 これは言語の文法で要求されているからだけでなく、wait および notify が適切に直列化されることを保証します。 この場合は、これにより競合状態が回避されます。つまり、「中断された」スレッドが notify を検出できずに、ずっと中断されたままになる事態を避けられます。

Java において同期化に要するコストは、プラットフォームが成熟するにつれて減少していますが、まったくなくなることはありません。 簡単な技法を使用して、上の「実行ループ」の反復処理に追加した同期処理を省くことができます。 追加された同期ブロックは、スレッドが実際に中断されている場合にのみ同期ブロックを入力する、わずかに複雑なコードに置き換えられます。

                if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
この結果、run メソッドは次のようになります。
    public void run() {
        while (true) {
            try{
                Thread.currentThread().sleep(interval);

                if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }
明示的な同期化を行わない場合は、threadSuspendedvolatile に設定して、中断要求の通信が速やかに行われるようにしなければなりません。


今までの 2 つの手法を組み合わせれば、安全に「停止」または「中断」させることのできるスレッドを生成できますか

はい、かなり簡単に生成できます。 1 つだけ問題になるのは、別のスレッドがターゲットスレッドを停止させようとする時点で、ターゲットスレッドがすでに中断されている可能性があるという点です。 stop メソッドが状態変数 (blinker) を null に設定するだけであれば、ターゲットスレッドは終了 (本来の動作) せずに、中断されたまま (モニターを待機した状態) になります。 アプレットが再起動されると、複数のスレッドが同時にモニターを待機した状態で終了するため、動作が異常になります。

この状況を正すには、stop メソッドで、ターゲットスレッドが中断されている場合に、そのスレッドをただちに再開する処理を確実に実行しなければなりません。 ターゲットスレッドは、再開したのち、自身が停止されたことをただちに認識して、適切な方法で終了しなければなりません。 そのような処理を加えた run および stop メソッドは、次のようになります。

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try{
                thisThread.sleep(interval);

                synchronized(this) {
                    while (threadSuspended && blinker==thisThread)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

    public synchronized void stop() {
        blinker = null;
        notify();
    }
stop メソッドで Thread.interrupt を呼び出す場合 (以前に説明した方法) は、notify を呼び出す必要はありませんが、同期化は行う必要があります。 それにより、競合状態のためにターゲットスレッドが割り込みを検出し損ねることを防げます。


Thread.destroy についてはどうですか

Thread.destroy は、これまで実装されたことがありません。 もし実装されたとすると、Thread.suspend の場合と同様に、デッドロックを発生させやすい傾向があります。 実際、Thread.suspend は、Thread.resume が続く可能性のない Thread.suspend とほとんど同じです。 現時点で Thread.destroy は実装されていませんが、これを推奨しないわけでもありません (将来それが実装された場合)。 Thread.destroy には確かにデッドロックを発生させやすい傾向があります。しかし、プログラムが即座に終了するよりもデッドロックの危険を冒してもよいという状況があるのではないかという点が、これまで論議の対象になってきました。


Runtime.runFinalizersOnExit が推奨されないのはなぜですか

本質的に安全ではないからです。 ファイナライザがライブオブジェクトに対して呼び出される結果になる可能性があり、その時ほかのスレッドがそれらのオブジェクトを同時に操作していると、動作が異常になるか、デッドロックが発生します。 この問題は、オブジェクトがファイナライズされるクラスがこの呼び出しに対して「防御される」ようにコーディングされていれば防ぐことができますが、ほとんどのプログラマは、この呼び出しに対して防御しません。 ほとんどのプログラマは、ファイナライザが呼び出される時点でそのオブジェクトは死んでいると想定します。

さらに、その呼び出しは、VM グローバルフラグを設定するという意味で、「スレッドに対して安全」ではありません。 これは、ファイナライザを持つあらゆるクラスを、ライブオブジェクトのファイナライズに対して防御するようにコーディングしなければならないことにつながります。


Copyright © 1995-2001 Sun Microsystems, Inc. All Rights Reserved.

機能の提案またはコメントの送り先
Sun

Java ソフトウェア