Java

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


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

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.suspend および Thread.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 つの手法を組み合わせて使用することはできますか。

簡単にできます。 問題になるのは、別のスレッドがターゲットスレッドを停止させようとする時に、ターゲットスレッドがすでに一時停止している場合です。 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.resume が来る可能性を除いて、Thread.suspend とほぼ同じです。 今回は Thread.destroy を実装していませんが、これを推奨しないわけではありません。 将来この実装を実現する予定です。 Thread.destroy には確かにデッドロックを発生させる傾向がありますが、プログラムが即座に終了するよりもデッドロックの危険を冒してもよい状況があるという論議があります。


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

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

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


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

コメントの送付先: jdk-comments@java.sun.com
Sun

Java ソフトウェア