機械翻訳について

Java非推奨スレッド・プリミティブ


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メソッドから通常の方法で復帰する必要があります。 停止要求の即時通信を確実にするには、変数をvolatileにする(または、変数へのアクセスを同期化する)必要があります。

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

    private Thread blinker;

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

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

    public void run() {
        while (true) {
            try {
                Thread.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 {
                Thread.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.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();
                    }
                }

明示的な同期化を行わない場合は、threadSuspendedvolatileに設定して、一時停止要求の通信がすみやかに行われるようにしてください。

結果のrunメソッドは次のようになります。
    private volatile boolean threadSuspended;

    public void run() {
        while (true) {
            try {
                Thread.sleep(interval);

                if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

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

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

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

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                Thread.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とほとんど同じです。

Runtime.runFinalizersOnExitが非推奨なのはなぜですか

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

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