Th-07: Condition + Lock による待機・通知パターン
ReentrantLock の Condition を使うと、synchronized のwait() / notify() より細かい待機・通知の制御ができます。 プロデューサーとコンシューマーでそれぞれ別の Condition を持ち、 「満杯なら待つ」「空なら待つ」を分離できます。
wait/notify との違い
synchronized の wait() / notifyAll() では、 「どの条件で待っているか」を区別できません。一方 Condition は複数作成できるため、 「キューが満杯のときだけプロデューサーを待たせる条件」と「キューが空のときだけコンシューマーを待たせる条件」を分けられます。
| 機能 | synchronized + wait/notify | Lock + Condition |
|---|---|---|
| 複数の待機条件 | ❌(1つのオブジェクトに1セット) | ✅(lock.newCondition() で複数作成可) |
| タイムアウト付き待機 | △(wait(timeout) は使いにくい) | ✅(awaitNanos() / awaitUntil()) |
| 記述のシンプルさ | ◎(シンプル) | △(unlock() を finally に書く必要あり) |
プロデューサー・コンシューマーパターン
「生産者(プロデューサー)がデータを生産し、消費者(コンシューマー)が取り出す」という典型的な非同期処理パターンです。 キューが満杯のときはプロデューサーを待機させ、空のときはコンシューマーを待機させます。Condition を2つに分けることで、プロデューサーとコンシューマーを独立して起こすことができます。
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // プロデューサー用
Condition notEmpty = lock.newCondition(); // コンシューマー用
// プロデューサー: 満杯なら待機
lock.lock();
try {
while (queue.size() == capacity) notFull.await();
queue.addLast(item);
notEmpty.signalAll(); // コンシューマーを起こす
} finally { lock.unlock(); }
// コンシューマー: 空なら待機
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
T item = queue.removeFirst();
notFull.signalAll(); // プロデューサーを起こす
} finally { lock.unlock(); }サンプルコード
Condition は ReentrantLock の newCondition() で取得します。await() は現在のスレッドを待機させ、signalAll() は待機中のすべてのスレッドを起こします。while ループで待機することで、スプリアスウェイクアップ(spurious wakeup: 予期せぬ目覚め)に対処できます。
よくあるミス・注意点
⚠️ await() は if ではなく while ループの中で呼ぶ
スプリアスウェイクアップ(spurious wakeup)と呼ばれる現象により、 シグナルを受けていないのに await() が戻ることがあります。if (queue.isEmpty()) ではなく while (queue.isEmpty()) にしておくと、 起きた後も条件を再チェックするため安全です。
// ❌ if だとスプリアスウェイクアップに対応できない if (queue.isEmpty()) notEmpty.await(); // ✅ while で条件を再チェック while (queue.isEmpty()) notEmpty.await();
⚠️ Condition は必ず対応する Lock の中で await() / signalAll() を呼ぶ
Condition.await() や signalAll() は、 その Condition を生成した Lock を保持している状態でのみ呼び出せます。 Lock の外で呼ぶと IllegalMonitorStateException が発生します。 必ず lock.lock() と lock.unlock()(finally)で囲んでください。
テストする観点
- ✅ プロデューサーが 5個追加し、コンシューマーが 5個取得した後にキューが空になること
- ✅ キューが満杯(capacity=3)のときにプロデューサーが待機し、コンシューマーが取り出した後に再開すること
- ✅ キューが空のときにコンシューマーが待機し、プロデューサーが追加した後に再開すること
- ✅ タイムアウト付き poll() でキューが空のまま指定時間を過ぎると null が返ること(境界値: タイムアウト直前・直後)
- ✅ Lock の外で Condition.await() を呼ぶと
IllegalMonitorStateExceptionが発生すること