Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · staff level

Condition variables and the producer–consumer problem: why must you wait in a loop, and what is a lost wakeup?

🎤 Say this first

A condition variable lets a thread sleep until a predicate becomes true, releasing the associated mutex atomically while it waits — solving the 'unlock then sleep' race (lost wakeup: the signal fires in the gap and you sleep forever). Two iron rules: (1) always wait in a while loop re-checking the predicate — wakeups can be spurious, and another thread may have consumed the state between signal and your re-acquisition of the lock; (2) signal/broadcast while the state change is protected. This mutex+condition+predicate triple is the bounded buffer — and it's exactly Object.wait/notify and Condition.await/signal in Java.

The full picture

The canonical bounded buffer (every rule visible)
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's sync.Cond, pthreads, and even Kafka consumer group coordination are predicate-wait patterns — learn it once as mutex+condition+predicate.

🔄 Likely follow-up questions

  • Why does await() need to reacquire the lock before returning?
  • When would you choose two conditions over notifyAll on one monitor?
  • How does LinkedBlockingQueue get away with two separate locks?