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