並列処理
でえらく盛り上がってしまったので一旦まとめたいと思います。
こちらのエントリではObject#wait()についてのまとめです。
waitの使い方
Object#wait()を呼び出すことで現在のThreadを停止させることが出来ますが、
この際のObjectのインスタンスは
Javaの並列処理の基本
の稿で説明したロックオブジェクトを用います。
wait()を呼び出す前に、対象となるオブジェクトをロックオブジェクトとして
synchronizedブロックを作らねばなりません。
Object lock = new Object();
// ロックオブジェクトでsynchronizedブロックを作る
synchronized (lock) {
// ロックオブジェクトのwait()を呼び出す
lock.wait();
}
synchronizedブロックの外でwait()を呼んだ場合は
IllegalMonitorStateExceptionがthrowされます。
Object#notify()およびObject#notifyAll()でwait()しているスレッドは動き出します。
このnotifyのインスタンスはもちろん、waitするときに利用したロックオブジェクトです。
notify()もまた、自身をロックオブジェクトとしたsynchronizedブロック内で
呼び出さないとIllegalMonitorStateExceptionをthrowします。
ロックオブジェクトをうまく管理すれば、停止再開を行う箇所をグルーピングできるわけです。
spurious wakeup
スプリアス・ウェイクアップと呼ばれる現象が知られています。
本来起きるべきではないタイミングでwait()から目覚めることがある、という現象です。
この現象については
Object#wait()のjavadocにも記載があります。
スレッドは、通知、割り込み、タイムアウトなしに再開されることがあります。 これは、「スプリアスウェイクアップ」と呼ばれています。スプリアスウェイクアップは、実際にはまれにしか発生しませんが、アプリケーションでは、スレッドが再開されることで発生する可能性がある条件をテストし、条件が満たされない場合は待機を続けて、スプリアスウェイクアップから保護しなければなりません。つまり、次のようにループで常に待機が発生するようにする必要があります。
synchronized (obj) {
while ()
obj.wait(timeout);
... // Perform action appropriate to condition
}
(このトピックの詳細は、Doug Lea 著『Concurrent Programming in Java (Second Edition)』(Addison-Wesley, 2000) のセクション 3.2.3 や Joshua Bloch 著『Effective Java Programming Language Guide』(Addison-Wesley, 2001) のアイテム 50 を参照してください)。
ここで引き合いに出されているEffective Javaでは
待ちスレッドは、通知がなくても起きるかもしれません。これは、偽りの目覚め(sputious wakeup)と呼ばれます。『Java 言語仕様』[JLS]がその可能性について述べていないにもかかわらず、多くのJVM実装は、たとえ稀であるにせよ、偽りの目覚めが起きることが知られているスレッド機構を使用しています[Posix, 11.4.3.6.1]。
と記述されています。([]の記述は参考文献)
実際、Java言語仕様(日本語版訳第3版)の
506ページ、「17.8待機集合と通知」にはスプリアス・ウェイクアップの記述はありません。
また、Java並行処理プログラミングでも
337ページ、「14-2-2 早すぎるウェイト終了」にて紹介されています。(yamasaさま、情報ありがとうございます)
我々が気をつけなければならないこと
正に問題となるのは以下のようなコードです。
synchronized (this.lock) {
// 終了前ならwait
if (!this.flag) {
this.lock.wait();
}
// 値を返す
return this.value;
}
フラグが立っていなければwaitし、どこか別のスレッドでこのフラグが立てられ、
notifyされて動き出すことを期待しています。
しかし、ここで前述のスプリアス・ウェイクアップが起きるとしたら、
まだthis.flagがfalseのうちに動き出し、
値をreturnしてしまう可能性があるのです!
そのため、以下のようにwhileループを用いて
不意に動き出したときの対策をしなければなりません。
synchronized (this.lock) {
// 終了前ならwait
while (!this.flag) {
this.lock.wait();
}
// 値を返す
return this.value;
}
javadocに記載されたミステリー
Object#wait()は3つのオーバーロードを持っていますが、
引数なしのオーバーロードには以下の記述があります。
引数が 1 つのバージョンでは、割り込みやスプリアスウェイクアップが発生する可能性があるので、このメソッドは常にループで使用される必要があります。
引数なしのwait()ではスプリアス・ウェイクアップが起きないかのような記述です。
日本語訳の誤りかと思ったのですが、
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
英語版の記述が上記通りなので、翻訳の誤りではないようです。
このあたりはEffective Javaの言うところの「偽りの目覚めが起きることが知られているスレッド機構」
とやらを調べて見ないと結論できないように思います。
投稿日時 : 2007年8月22日 11:46