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もコールする必要はありませんが、同期する必要があります。 それにより、競合状態のためにターゲット・スレッドが割込みを検出し損ねることを防げます。