The full picture
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Deque<Item> buf = new ArrayDeque<>(CAP);
void put(Item item) throws InterruptedException {
lock.lock();
try {
while (buf.size() == CAP) // WHILE, never if: spurious wakeups +
notFull.await(); // the state may be re-consumed before we run
buf.addLast(item); // await releases the lock atomically -> no lost wakeup
notEmpty.signal(); // signal while holding the lock, state is consistent
} finally { lock.unlock(); }
}
// Two conditions on one lock = producers and consumers don't wake each other
// pointlessly — the exact structure inside ArrayBlockingQueue.- Lost wakeup, precisely: check-empty → (unlock) → producer signals here, nobody listening → sleep = forever. The atomic release-and-wait of
await()closes that window; hand-rolled flag-and-sleep does not — this is THE reason condition variables exist as a primitive. - Spurious wakeups are contractual (POSIX and Java both permit them) — partly implementation artifacts, partly the deliberate cost of cheaper signaling. The while-loop makes them harmless, so the standard makes them legal.
- signal vs broadcast (notify/notifyAll): signal wakes one — efficient but only safe when any waiter can handle the state; broadcast wakes all — correct-by-default when waiters wait for different predicates on one condition (or just use separate conditions, as above).
- Same shape everywhere:
wait/notify(monitor form), Go'ssync.Cond, pthreads, and even Kafka consumer group coordination are predicate-wait patterns — learn it once as mutex+condition+predicate.