Object.wait()メソッドは、ロックの所有権を一時的に放棄することで、ロックを要求する他のスレッドが処理を進めることを可能にする。Object.wait()メソッドは必ず、synchronizedブロックあるいはsynchronizedメソッドから呼び出さなくてはならない。待機スレッドは、 他のスレッドがnotify()メソッドやNotifyAll()メソッドを呼び出して通知を行った場合にのみ、実行を再開する。 wait()メソッドの呼出しは、条件述語が真であるかどうかをチェックするループ処理内で行われるべきである。 注意すべきは、条件述語がループの条件式を否定した形をとるという点である。たとえば、ベクターから要素を削除する場合の条件述語は、!isEmpty() であるが、while ループの条件式は isEmpty() になる。以下のコードに、ベクターが空である場合に正しくwait()メソッドを呼び出す方法を示す。
private Vector vector; //... public void consumeElement() throws InterruptedException { synchronized (vector) { while (vector.isEmpty()) { vector.wait(); } // 条件が成り立つ場合、処理を再開する } }
通知の仕組みにより待機スレッドへ通知が行われ、待機スレッドは条件述語をチェックする。別スレッドで呼び出すnotify()メソッドやnotifyAll()メソッドにおいて、どの待機スレッドを再開するかを指定することはできない。通知を受けたスレッッドが、処理を再開すべきか否か判断することが可能になるだけである。条件述語が有効なケースとしては、条件が真になるまでの間あるスレッドの処理を待機させたい場合、たとえばデータをリードする前に入力ストリームにデータが到着するのを待つ場合、などが考えられる。
wait/notify の仕組みを使用する場合、安全性(safety)と活性(liveness)に配慮することが重要になる。安全性とは、すべてのオブジェクトが、マルチスレッド環境下で首尾一貫した状態を保持することである[Lea 2000]。活性とは、すべての処理やメソッド呼出しが中断せずに完了することである。
活性を保証するには、wait()メソッドを呼び出す前に while ループの条件を評価しなければならない。このように先行して条件を評価することで、他のスレッドが条件述語を満たし、既に通知を送信しているかどうかを確認することができる。通知が既に送信された後でwait()メソッドを呼び出してしまうと、いつまでも待機状態のままになってしまう。
安全性を保証するには、wait()メソッドの呼出しから返った後にも while ループ条件を評価しなくてはならない。wait()メソッドは本来、通知を受信するまでスレッドを待機状態にしているが、次に挙げる脆弱性を防止するためにループの中に記述する必要がある[Bloch 2001]。
- 中間スレッド(thread in the middle) - あるスレッドが通知を送信してから通知を受信したスレッドが実行を再開するまでの間に、第三のスレッドが共有オブジェクトのロックを獲得できる可能性がある。第三のスレッドは、オブジェクトの状態を変更し矛盾した状態にすることができる。これは TOCTOU 脆弱性である。
- 悪意ある通知(malicious notification) - 条件述語が偽であるときに、任意にあるいは悪意をもって送信される通知を受信するかもしれない。このような通知が行われると、wait()メソッドの呼出しは無効になる。
- 通知の誤配信(misdelivered notification) - notifyAll()メソッドからの通知を受け取ったスレッドがどの順序で実行されるかは決まっていない。したがって、無関係なスレッドが条件述語が真の状態で再開される可能性がある。結果的に、本来は休止状態を維持すべきスレッドが、実行を再開するかもしれない。
- 見せかけの起動(spurious wakeups) - 通知がなくとも待機状態のスレッドを開始してしまう、見せかけの起動が発生しうるJVM実装が存在する [API 2006]。
これらの理由から、条件述語は、wait()メソッドを呼び出した後にもチェックする必要がある。wait()メソッドの呼出し前後に条件述語をチェックするには、whileループを使用するのが最適である。
同様に、Conditionインタフェースのawait()メソッドもループ内で呼び出さなくてはならない。Conditionインタフェースについて、Java API仕様は以下のように記述している[API 2006]。
プラットフォームセマンティクスへの譲歩として、Condition の待機中に「見せかけの起動」が発生することが許されている。Conditionは、常にループ上で待機させ、待機対象の条件述語を確認する形で使用すべきなので、大抵のアプリケーションプログラムでは、見せかけの起動による実質的な影響はほとんどない。JVMの実装において見せかけの起動が発生しないようにするのは自由だが、アプリケーションプログラマは、見せかけの起動が発生しうることを前提にループ上で常に待機する対応を取ることが推奨される。
今後新たに作成するソースコードでは、wait/notify の仕組みの代わりに java.util.concurrentパッケージの並行処理用ユーティリティを使用するべきである。しかし、このルールのその他の要求に従うレガシーコードでは wait/notify の仕組みに依存しても構わない。
違反コード
以下の違反コードでは、wait()メソッドをif節の中で呼び出しており、通知の受信後に事後条件のチェックを行っていない。通知が誤配信されたり悪意ある通知が行われた場合、条件述語が成立していない状態でスレッドを再開してしまう可能性がある。
synchronized (object) { if (<条件が成立しない>) { object.wait(); } // 条件が成立したときに実行される }
適合コード
以下の適合コードでは、whileループの内部でwait()メソッドを呼ぶことにより、wait()メソッド呼出しの前後で条件をチェックしている。
synchronized (object) { while (<条件が成立しない>) { object.wait(); } // 条件が成立したときに実行される }
java.util.concurrent.locks.Condition.await()メソッドの呼出しも同様に、ループの中で行わなくてはならない。
リスク評価
wait()メソッドやawait()メソッドをwhileループの中で呼び出さないと、いつまでも待機状態のままになり、サービス運用妨害(DoS)を引き起こす恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
THI03-J | 低 | 低 | 中 | P2 | L3 |
参考文献
[API 2006] | Class Object |
[Bloch 2001] | Item 50. Never invoke wait outside a loop |
[Lea 2000] | 3.2.2, Monitor Mechanics; 1.3.2, Liveness |
[Goetz 2006] | Section 14.2, Using Condition Queues |
翻訳元
これは以下のページを翻訳したものです。
THI03-J. Always invoke wait() and await() methods inside a loop (revision 85)