Definition
- Spurious wake-ups – the JVM specification (JLS 17.8.1) allows
wait()to return withoutnotify()being called. This is rare but legal. - Stolen conditions – after
notify(), the woken thread must re-acquire the lock. During that window, another thread may acquire the lock first and change the state (e.g., consume the last item). - while loop – rechecks the condition after every wake-up. If the condition is no longer true, the thread goes back to
wait(). - if statement – checks the condition once. If a spurious wake-up occurs or the condition is stolen, the thread proceeds with a false assumption.
Using if: the manager calls you from the break room saying "A customer arrived!" You walk to the counter (re-acquire lock). But while you were walking, another teller served that customer. You arrive and find no customer. With if, you start processing nothing and crash. With while, you check again: "Is there a customer?" No, so you go back to the break room.
Understanding the Concepts
What goes wrong with if?
Using if checks the condition once. After waking from wait(), the thread proceeds without re-checking. If a spurious wake-up occurred or another thread consumed the item, the condition is no longer true.
package com.bank.core;
import java.util.LinkedList;
import java.util.Queue;
public class IfVsWhileDemo {
private static final Object lock = new Object();
private static final Queue<String> queue = new LinkedList<>();
// WRONG: if checks condition once
public static String wrongConsume() throws InterruptedException {
synchronized (lock) {
if (queue.isEmpty()) {
lock.wait();
}
// DANGER: queue might STILL be empty!
// Spurious wake-up or another consumer took the item.
return queue.poll(); // returns null -> NullPointerException!
}
}
// CORRECT: while rechecks after every wake-up
public static String correctConsume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
// Guaranteed: queue is NOT empty
return queue.poll();
}
}
}
What is a spurious wake-up?
The JVM specification (JLS 17.8.1) explicitly states: "A thread may also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup." This happens because the underlying OS implementation (pthread_cond_wait on Linux) may return spuriously due to signals or kernel scheduling decisions. It is rare but legally permitted, and code that does not handle it is formally incorrect.
What is a stolen condition?
Even without spurious wake-ups, the condition can change between notify() and lock re-acquisition.
Timeline with 3 threads:
1. Consumer-A and Consumer-B both waiting (queue empty)
2. Producer adds one item, calls notifyAll()
3. Consumer-A wakes up, moves to entry set, competes for lock
4. Consumer-B also wakes up, competes for lock
5. Consumer-B acquires lock FIRST, polls the item, releases lock
6. Consumer-A acquires lock, but queue is EMPTY!
With if: queue.poll() returns null -> NPE or wrong logic!
With while: rechecks isEmpty() -> true -> goes back to wait()
What is the canonical pattern?
This pattern works for both wait() and Condition.await(). It is the only correct way to use wait-based synchronization.
synchronized (lock) {
while (!condition) {
lock.wait();
}
// condition is GUARANTEED true here
doWork();
}
How does this apply to a banking producer-consumer?
package com.bank.queue;
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.Queue;
public class SettlementQueue {
private final Object lock = new Object();
private final Queue<Settlement> queue = new LinkedList<>();
private static final int MAX_SIZE = 100;
public void produce(Settlement settlement) throws InterruptedException {
synchronized (lock) {
while (queue.size() >= MAX_SIZE) {
lock.wait(); // wait until space available
}
queue.add(settlement);
lock.notifyAll(); // wake consumers
}
}
public Settlement consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // wait until item available
}
Settlement s = queue.poll();
lock.notifyAll(); // wake producers waiting for space
return s;
}
}
}
How does Condition.await() relate to this pattern?
The Condition interface from java.util.concurrent.locks provides the same wait/notify mechanism with finer-grained control. The same while-loop rule applies.
package com.bank.queue;
import java.util.concurrent.locks.*;
public class ConditionQueueExample {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public void produce(Object item) throws InterruptedException {
lock.lock();
try {
while (isFull()) notFull.await(); // while, not if!
addItem(item);
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
What should a senior engineer know?
- JLS 17.8.1: the specification explicitly allows spurious wake-ups. Any code relying on
ifinstead ofwhileis formally incorrect per the spec. - Testing: spurious wake-ups are rare in practice and you cannot reliably trigger them in tests. Code review and static analysis are the primary defenses.
- FindBugs/SpotBugs: the WA_NOT_IN_LOOP detector flags
wait()not inside a while loop. Add this to your CI pipeline.
What are common interview follow-up questions?
- Does the same rule apply to Condition.await()? Yes, always use while with await().
- Can you use notifyAll instead of notify? notifyAll is safer because it wakes all waiters. notify wakes only one, which may not be the one whose condition is satisfied.
- How does BlockingQueue avoid this problem? BlockingQueue handles the while-loop and condition checking internally. Users call put() and take() without writing the loop.
Visual Overview
if (wrong): while (correct): ┌───────────────────┐ ┌───────────────────┐ │ check condition │ │ check condition │ │ false: wait() │ │ false: wait() │ │ wakes up │ │ wakes up │ │ proceed (no check!)│ │ recheck condition │ │ DANGER: condition │ │ still false? wait()│ │ may have changed! │ │ true? proceed! │ └───────────────────┘ └───────────────────┘
Key Takeaway
wait() must always be inside a while loop, never an if statement. Two reasons: spurious wake-ups (the JVM allows wait() to return without notify()) and stolen conditions (another thread changes the state between notify() and lock re-acquisition). The while loop rechecks the condition after every wake-up, catching both cases.
The canonical pattern is: synchronized(lock) { while (!condition) { lock.wait(); } doWork(); }. Using if instead of while can cause NullPointerException, ArrayIndexOutOfBoundsException, or incorrect business logic when the condition is no longer true.
Remember one sentence: always use while (not if) around wait() because spurious wake-ups and stolen conditions can make the condition false when the thread resumes.
---