Java非推奨スレッド・プリミティブ
Thread.stop
が非推奨になり、スレッドを停止する機能が削除されたのはなぜですか。
本来は安全ではないからです。 スレッドを停止すると、ロックされたすべてのモニターのロックが解除されました。 (ThreadDeath
例外がスタックに伝播されるため、モニターはロック解除されました。) これらのモニターによって以前に保護されていたいずれかのオブジェクトが一貫性のない状態であった場合、他のスレッドはこれらのオブジェクトを一貫性のない状態で表示している可能性があります。 そのようなオブジェクトは、壊れたオブジェクトと呼ばれます。 壊れたオブジェクトに対してスレッドが操作を実行すると、予期しない結果になる可能性があります。 この動作は、微妙で検出が困難な場合と、はっきりと通知される場合があります。 他の未チェックの例外とは異なり、ThreadDeath
はサイレントにスレッドを強制終了しました。したがって、ユーザーは、プログラムが破損している可能性があることを警告しませんでした。 破損は、実際の損傷が発生したあと、数時間または数日後に発生する可能性があります。
ThreadDeath
を捕捉して破損したオブジェクトを修正することはできませんでしたか。
理論的には、おそらく可能です。ただし、正しいマルチスレッド・コードを記述するのは非常に複雑なタスクです。 これがほとんど実行不可能なタスクであることは、次の2つの理由によります。
- スレッドは
ThreadDeath
例外をほぼどこでもスローできます。 このことを念頭に置いて、すべての同期化されたメソッドおよびブロックを詳細に調査する必要がある。 - スレッドは、最初の(
catch
またはfinally
句)からのクリーンアップ中に2番目のThreadDeath
例外をスローする可能性があります。 クリーンアップは成功するまで繰り返さなければならないでしょう。 この動作を確実に行うコードは非常に複雑になる。
Thread.stop
の代わりに何を使うべきですか
多くの場合、ターゲット・スレッドの実行停止を指示するには、stop
ではなく、単に一部の変数を変更するコードを使用する必要があります。 ターゲット・スレッドは、この変数を定期的に検査し、実行を停止するべきことを変数が示している場合には、スレッドのrunメソッドから通常の方法で復帰する必要があります。 stop-requestのプロンプト通信を保証するには、変数を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() { 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.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.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(); } }
明示的な同期がない場合、suspend-requestのプロンプト通信を確保するために、threadSuspended
をvolatile
にする必要があります。
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
もコールする必要はありませんが、同期する必要があります。 それにより、競合状態のためにターゲット・スレッドが割込みを検出し損ねることを防げます。