|
Thread.stop
が推奨されないのはなぜですか本質的に安全ではないからです。 スレッドを停止すると、そのスレッドがロックしたすべてのモニターのロックが解除されます。 (ThreadDeath
例外がスタックまで伝わると、モニターのロックが解除される。) これらのモニターによって以前保護されていたオブジェクトが整合性のない状態になると、ほかのスレッドも、これらのオブジェクトが整合性のない状態にあると見なします。 そのようなオブジェクトは、「壊れた」オブジェクトと呼ばれます。 壊れたオブジェクトに対してスレッドが操作を実行すると、予期しない結果になる可能性があります。 この動作は、微妙で検出が困難な場合と、はっきりと通知される場合があります。 チェックされないほかの例外とは異なり、ThreadDeath
は、スレッドをそのまま強制的に終了します。このため、ユーザは、プログラムが壊れる可能性を警告されることがありません。 プログラムが壊れていることは、実際に損傷を受けたしばらくあとに明らかになり、それが数時間後または数日後になることもあります。
ThreadDeath
例外をキャッチし、壊れたオブジェクトを修復することはできないのですか理論的には、おそらく可能です。ただし、正しいマルチスレッドコードを記述するのは「非常に」複雑な作業です。 これがほとんど実行不可能な作業であることは、次の 2 つの理由によります。
ThreadDeath
例外を「ほとんどすべての場所で」スローする。 このことを念頭に置いて、すべての同期化されたメソッドおよびブロックを詳細に調査する必要がある
catch
または finally
節) に、2 番目の ThreadDeath
をスローすることがある。 クリーンアップは、正常に終了するまで繰り返し実行される必要がある。 この動作を確実に行うコードは非常に複雑になる
Thread.stop(Throwable)
についてはどうですか上で説明した問題すべてに加えて、このメソッドは、対象とするスレッドの処理準備ができていないという例外 (このメソッドがなかったならスレッドがスローできないはずの、チェックされる例外を含む) を発生させるのに使用されることがあります。 たとえば、次のメソッドの動作は、Java の throw
操作と同じですが、呼び出し側のメソッドがスローする可能性のあるチェックされる例外すべてが宣言されていることを保証しようとするコンパイラをだまして失敗させます。
static void sneakyThrow(Throwable t) { Thread.currentThread().stop(t); }
Thread.stop
の代わりに何を使うべきですかstop
の代わりとしては、多くの場合、ターゲットスレッドの実行を停止するべきことを示す何らかの変数を単に変更するコードを使用します。 ターゲットスレッドは、この変数を定期的に検査し、実行を停止するべきことを変数が示している場合には、スレッドの run メソッドから通常の方法で復帰する必要があります。 これは、Java ソフトウェアのチュートリアルで常に推奨している方法です。 停止要求の即時通信を確実にするには、変数を volatile にする (または、変数へのアクセスを同期化する) 必要があります。
たとえば、アプレットに次の start
、stop
、および 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(); } }明示的な同期化を行わない場合は、threadSuspended を volatile に設定して、中断要求の通信が速やかに行われるようにしなければなりません。
この状況を正すには、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. 機能の提案またはコメントの送り先 |
Java ソフトウェア |