The rule: always while, never if
Every guarded wait on a condition variable should look like this, in any language:
acquire(lock)
while not predicate_holds():
wait(condition_variable) # atomically releases lock, blocks, reacquires lock on return
# predicate_holds() is now guaranteed true
do_the_work()
release(lock)
Not this:
# WRONG in essentially every mainstream language
acquire(lock)
if not predicate_holds():
wait(condition_variable)
do_the_work() # predicate might be false here!
release(lock)
This subtopic is about the two independent reasons the while form is mandatory — one of them is a documented, spec-level guarantee you should be able to cite by name, and the other is a straightforward consequence of the Mesa-style scheduling covered in the previous subtopic. Interviewers who ask "why do we loop here?" are usually listening for both, not just one.
Reason 1: spurious wakeups are real and permitted by spec
A spurious wakeup is when wait() returns even though nobody called notify/signal for that condition variable — no state change happened, no other thread did anything relevant. This sounds like it shouldn't be allowed, but it explicitly is, by design, in essentially every mainstream threading spec:
- POSIX (
pthread_cond_wait): the standard explicitly permits implementations to occasionally wake a waiting thread without a corresponding signal, as a concession to how some platforms implement condition variables efficiently (e.g., to avoid extra bookkeeping when handling POSIX signals or multiprocessor cache effects).
- Java (
Object.wait() and java.util.concurrent.locks.Condition.await()): both are documented to allow "a thread to wake up without being notified, interrupted, or timing out." The Object.wait() Javadoc even calls this out by name and shows the while-loop pattern as the required fix, in the same paragraph.
- Python (
threading.Condition.wait()): implemented on top of the platform's native primitives, so it inherits this same allowance — Python's own documentation and standard idioms use the while form for exactly this reason.
Why would any implementation actually do this? A few reasons show up in practice: some OS-level condition variable implementations are built on lower-level signaling primitives (like POSIX signals) that can themselves misfire; some multiprocessor implementations trade a small chance of a spurious wakeup for cheaper, lock-free wake paths; and permitting it in the spec at all gives implementers room to optimize without being held to a stricter guarantee they'd otherwise have to pay for on every call. Spurious wakeups are rare in practice on any given platform, but "rare" is not "impossible," and correctness code cannot depend on probabilistic behavior — a guard that's wrong 1 time in a billion is still wrong.
Reason 2: even without spurious wakeups, the resource can be gone by the time you wake up
This is the reason that actually bites people in production, far more often than a genuine spurious wakeup ever will, and it follows directly from the Mesa-style signal-and-continue semantics from the previous subtopic. Walk through this timeline for a bounded buffer with a single notEmpty condition:
- Consumer thread C acquires the lock, sees the buffer is empty, calls
wait() — releases the lock, blocks.
- Producer thread P acquires the lock, adds an item, calls
notify() (or notifyAll()). C is moved from "blocked" to "ready to run," but does not run yet — under Mesa semantics, P still holds the lock and keeps executing.
- P finishes its critical section and releases the lock.
- Before the OS scheduler gets around to running C, a third thread — call it C2, maybe another consumer, maybe even a thread that never called
wait() at all and is only now calling take() for the first time (this is sometimes called "barging") — acquires the freshly-released lock, sees the buffer is non-empty, and takes the item.
- C finally gets scheduled, reacquires the lock (which it's now free to do since C2 released it) , and returns from
wait(). If C trusted an if instead of a while, it would now try to remove an item from what might be an empty buffer.
Nothing here required a "spurious" wakeup in the technical sense — every notify was legitimate, every waiter was woken for a real reason. The problem is purely the gap between being woken and actually resuming execution with the lock held, during which any other thread with access to the same lock can change the state out from under you. This is a direct, unavoidable consequence of signal-and-continue scheduling, not an edge case you can engineer away — which is why the while-loop guard isn't a defensive nicety, it's load-bearing correctness logic.
Guard loops with multiple predicates: the bounded buffer
The producer-consumer / bounded-buffer problem is the canonical vehicle for practicing this, because it has two independent guard conditions sharing the same protected state (size, capacity):
put(item):
acquire(lock)
while size == capacity:
wait(notFull) # guard: buffer is full
add item; size += 1
signal(notEmpty) # a consumer's predicate may now be true
release(lock)
take():
acquire(lock)
while size == 0:
wait(notEmpty) # guard: buffer is empty
remove item; size -= 1
signal(notFull) # a producer's predicate may now be true
release(lock)
Two things to notice, both callback to the previous subtopic. First, whether you can safely use signal() here instead of signalAll()/notifyAll() depends entirely on whether your language gives each predicate its own condition variable (as this pseudocode assumes, and as java.util.concurrent.locks.Condition or POSIX's "one pthread_cond_t per predicate" convention both allow) or forces everyone waiting on the object to share one wait-set (as Java's intrinsic synchronized/wait/notifyAll does) — in the latter case you must broadcast, because a notify() might wake a producer when only a consumer's predicate changed, and that producer will just re-check, find size == capacity still true, and go back to sleep while the consumer that could have proceeded never gets a chance. Second, the while guard is what makes this correct even when threads race on which one gets to run first after a signal — exactly the barging scenario walked through above.
This shape — one lock, one or more condition variables, each with its own while (!predicate) wait() guard — is the monitor pattern from the previous subtopic in its most practically useful form, and it's the pattern nearly every "implement a thread-safe bounded/blocking structure" interview question (see the problems below) is really testing.
Pitfalls and interview gotchas
- Using
if instead of while. The single most common bug in guarded-wait code, and the one interviewers most often probe for directly by asking "what if the OS wakes this thread spuriously?" or "what if two consumers were both waiting?"
- Checking the condition without holding the lock. The predicate and the lock are inseparable — reading shared state to decide whether to wait, without holding the lock that protects that state, reintroduces the exact race
wait()'s atomicity was designed to close.
- Forgetting that
notify/signal doesn't release the lock. In every mainstream API, the signaling thread keeps the lock until it explicitly releases it (falls out of the synchronized block, calls unlock(), etc.); the woken thread cannot actually resume — even after being marked ready — until the signaler lets go. Confusing "signaled" with "running now" is what leads people to (wrongly) assume Hoare semantics are in play.
- Assuming a single condition variable is fine for genuinely different predicates, when your language actually supports splitting them (as in the
Condition/pthread_cond_t-per-predicate style above) — needlessly broadcasting when you could signal precisely is a common performance smell in review, even though it isn't a correctness bug.
- Deadlocking by waiting while holding a second, unrelated lock.
wait() only releases the lock associated with that condition variable; if the waiting thread is also holding some other lock acquired earlier, that second lock stays held for the entire wait — a frequent source of surprise deadlocks in code that nests locking without thinking it through.