Java : Object.waitの注意点
Objectクラスには、現在のスレッドを待機させる wait メソッドがあります。
ただし、wait メソッドには「スプリアス・ウェイクアップ (spurious wakeup)」と呼ばれる注意すべき仕様があります。
本記事ではその注意すべき仕様と、代わりにおすすめする CountDownLatch クラスについてもご紹介します。
概要
Objectクラスには、現在のスレッドを待機させる wait と復帰するための notify メソッドがあります。
wait によるスレッドの待機は、通常…
- notify の呼び出し
- スレッドの中断(interrupt)
- タイムアウト
により復帰します。
しかし、上記の条件以外でも、「スプリアス・ウェイクアップ」によって復帰することがあります。
スプリアス・ウェイクアップは、まれにしか発生しない少し面倒な仕様です。
詳細は上記API仕様をご確認ください。
waitとnotifyの例
まずは、スプリアス・ウェイクアップを考慮しない例を見てみましょう。
// 基準となる時刻
final long current = System.nanoTime();
// 基準となる時刻からの差分を秒として取得します。
final DoubleSupplier elapsedSec = () -> (System.nanoTime() - current) / 1000000000d;
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
// 同期用のオブジェクトです。
final var obj = new Object();
// サブスレッド上で実行する処理(タスク)です。
final Runnable task = () -> {
try {
System.out.printf("task start : %f sec.\n", elapsedSec.getAsDouble());
synchronized (obj) {
obj.wait(); // ★
}
} catch (InterruptedException e) {
System.out.printf("InterruptedException発生! : %f sec.\n", elapsedSec.getAsDouble());
} finally {
System.out.printf("task end : %f sec.\n", elapsedSec.getAsDouble());
}
};
executorService.submit(task);
Thread.sleep(2000);
synchronized (obj) {
obj.notify(); // ★
}
} finally {
executorService.shutdown();
System.out.printf("-- shutdown -- : %f sec.\n", elapsedSec.getAsDouble());
}
final var result = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.printf("term %b : %f sec.\n", result, elapsedSec.getAsDouble());
おおまかなシーケンスは次のようになります。
サブスレッドで wait して、メインのスレッドから2秒後に notify します。
結果は次のようになります。
//task start : 0.002802 sec.
//task end : 2.004060 sec. <-- ★ここ
//-- shutdown -- : 2.004080 sec.
//term true : 2.004597 sec.
期待どおりに、タスクが2秒後に終わっているのが分かりますね。
しかし、スプリアス・ウェイクアップが発生すると、次のような結果になる可能性があります。
//task start : 0.002802 sec.
//task end : 1.000000 sec. <-- ★ここ
//-- shutdown -- : 2.004080 sec.
//term true : 2.004597 sec.
タスクが 2秒より前に終わってしまいます。
API仕様上、このようなことが起こる可能性はゼロではありません。
スプリアス・ウェイクアップを考慮した例
公式のAPIノートに、スプリアス・ウェイクアップを対処するおすすめの方法が紹介されています。
以下、抜粋します。
synchronized (obj) {
while (<condition does not hold> and <timeout not exceeded>) {
long timeoutMillis = ... ; // recompute timeout values
int nanos = ... ;
obj.wait(timeoutMillis, nanos);
}
... // Perform action appropriate to condition or timeout
}
それでは、もう少し具体的に実装してみましょう。
今回はタイムアウトは考慮せずに、待機条件だけを考慮してみます。
まずは同期用のクラスを新しく定義します。
public class SyncObj {
private boolean signaled;
// Object.waitの代わりとなるメソッドです。
public synchronized void await() throws InterruptedException {
// スプリアス・ウェイクアップが発生してwaitから復帰しても、
// signalメソッドが呼び出されていなければ、もう1度waitします。
while (!signaled) {
wait();
}
}
// Object.notifyの代わりとなるメソッドです。
public synchronized void signal() {
signaled = true;
notify();
}
}
そして、先ほどのコード例の同期用オブジェクトを、SyncObjに置き換えます。
// 基準となる時刻
final long current = System.nanoTime();
// 基準となる時刻からの差分を秒として取得します。
final DoubleSupplier elapsedSec = () -> (System.nanoTime() - current) / 1000000000d;
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
// 同期用のオブジェクトです。
final var syncObj = new SyncObj();
// サブスレッド上で実行する処理です。
final Runnable task = () -> {
try {
System.out.printf("task start : %f sec.\n", elapsedSec.getAsDouble());
syncObj.await(); // ★
} catch (InterruptedException e) {
System.out.printf("InterruptedException発生! : %f sec.\n", elapsedSec.getAsDouble());
} finally {
System.out.printf("task end : %f sec.\n", elapsedSec.getAsDouble());
}
};
executorService.submit(task);
Thread.sleep(2000);
syncObj.signal(); // ★
} finally {
executorService.shutdown();
System.out.printf("-- shutdown -- : %f sec.\n", elapsedSec.getAsDouble());
}
final var result = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.printf("term %b : %f sec.\n", result, elapsedSec.getAsDouble());
//task start : 0.003254 sec.
//task end : 2.016415 sec.
//-- shutdown -- : 2.016554 sec.
//term true : 2.017053 sec.
このコードであれば、スプリアス・ウェイクアップが発生しても、タスクは2秒後に終了します。
標準APIのCountDownLatchを使おう
先ほどは、SyncObjという独自のクラスを定義しましたが、標準APIにはもっとよいAPIが用意されています。
それが CountDownLatch クラスです。
シンプルで、汎用的に使えて良いAPIだと思います。
そして、API仕様的にスプリアス・ウェイクアップの影響は受けません。
以下は CountDownLatch を使った例になります。
// 基準となる時刻
final long current = System.nanoTime();
// 基準となる時刻からの差分を秒として取得します。
final DoubleSupplier elapsedSec = () -> (System.nanoTime() - current) / 1000000000d;
final ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
// 同期用のオブジェクトです。
final var latch = new CountDownLatch(1);
// サブスレッド上で実行する処理です。
final Runnable task = () -> {
try {
System.out.printf("task start : %f sec.\n", elapsedSec.getAsDouble());
latch.await(); // ★
} catch (InterruptedException e) {
System.out.printf("InterruptedException発生! : %f sec.\n", elapsedSec.getAsDouble());
} finally {
System.out.printf("task end : %f sec.\n", elapsedSec.getAsDouble());
}
};
executorService.submit(task);
Thread.sleep(2000);
latch.countDown(); // ★
} finally {
executorService.shutdown();
System.out.printf("-- shutdown -- : %f sec.\n", elapsedSec.getAsDouble());
}
final var result = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.printf("term %b : %f sec.\n", result, elapsedSec.getAsDouble());
//task start : 0.004346 sec.
//task end : 2.018145 sec.
//-- shutdown -- : 2.018165 sec.
//term true : 2.018829 sec.
もし、CountDownLatchで目的が達成できるのであれば、Object.waitではなく CountDownLatch を使うことをおすすめします。
オブジェクト・モニターを厳密に制御したい場合などは、Object.waitでないと実現できない…ということもあるかもしれません。
補足
スプリアス・ウェイクアップについて、外部サイトで参考となる記事をいくつかご紹介します。
- java - Is CountDownLatch affected by spurious wakeups? - Stack Overflow
- multithreading - Do spurious wakeups in Java actually happen? - Stack Overflow
まとめ
Object.wait には、スプリアス・ウェイクアップと呼ばれる少しやっかいな仕様があります。
もし、Object.waitを使う場合は、スプリアス・ウェイクアップを考慮した実装が必要となります。
もしくは、スプリアス・ウェイクアップの影響を受けない CountDownLatch を代わりに使うのもおすすめです。
関連記事
- API 使用例
- BlockingQueue (ブロッキング・キュー)
- Callable
- CancellationException
- ConcurrentHashMap.KeySetView (並列処理用セット)
- ConcurrentLinkedDeque (並列処理用・両端キュー)
- ConcurrentLinkedQueue (並列処理用キュー)
- ConcurrentMap (並列処理用マップ)
- ConcurrentModificationException (並列処理例外)
- ConcurrentSkipListSet (並列処理用セット)
- Condition (同期)
- CopyOnWriteArrayList (並列処理用リスト)
- CopyOnWriteArraySet (並列処理用セット)
- CountDownLatch (同期)
- CyclicBarrier (同期)
- Exchanger (同期)
- Executor
- ExecutorService
- Executors
- Future
- Future.State
- FutureTask
- InterruptedException (割込み例外)
- Lock (同期)
- Object (オブジェクト)
- Runnable
- Semaphore (セマフォ)
- Thread (スレッド)
- ThreadGroup
- ThreadLocal
- TimeUnit