This rule is simple and non-negotiable:
wait() must be used inside a loop that re-checks the condition.
If you remember only one guardrail for low-level monitor coordination, remember this one.
Problem Statement
A waiting thread wants to continue only if a condition is true.
Broken idea:
if (!condition) {
wait();
}
Correct idea:
while (!condition) {
wait();
}
Why is the loop necessary? Because waking up does not guarantee the condition is still valid.
Why if Is Wrong
Even after a thread wakes:
- another thread may have consumed the condition first
- the wakeup may not correspond to the condition this thread needs
- spurious wakeups are permitted
So “I woke up” is not the same as “it is now safe to proceed.”
The condition must be checked again under the same monitor.
Runnable Example
Broken version:
import java.util.LinkedList;
import java.util.Queue;
public class BrokenWaitIfDemo {
public static void main(String[] args) throws Exception {
MessageQueue queue = new MessageQueue();
Thread consumer = new Thread(queue::consumeBroken, "consumer");
Thread producer = new Thread(() -> queue.produce("task-1"), "producer");
consumer.start();
Thread.sleep(500);
producer.start();
consumer.join();
producer.join();
}
static final class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
synchronized void produce(String value) {
queue.add(value);
notifyAll();
}
synchronized void consumeBroken() {
if (queue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("Consumed " + queue.remove());
}
}
}
This may appear to work in easy runs. It is still wrong.
Correct Version
import java.util.LinkedList;
import java.util.Queue;
public class WaitLoopDemo {
public static void main(String[] args) throws Exception {
MessageQueue queue = new MessageQueue();
Thread consumer = new Thread(queue::consume, "consumer");
Thread producer = new Thread(() -> queue.produce("task-1"), "producer");
consumer.start();
Thread.sleep(500);
producer.start();
consumer.join();
producer.join();
}
static final class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
synchronized void produce(String value) {
queue.add(value);
notifyAll();
}
synchronized void consume() {
while (queue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("Consumed " + queue.remove());
}
}
}
This is the correct guarded-wait pattern.
Production-Style Example
Imagine a batching worker that waits until:
- queue size reaches threshold
- or shutdown requests final flush
The worker should not wake once and blindly continue. It must re-check:
- do I truly have enough work?
- did another worker already consume it?
- did the signal mean something else?
That is why loop-based waiting is standard, not optional style.
Why the Loop Preserves Correctness
The loop ties together:
- the monitor
- the condition
- the wakeup
- the re-check
Without the re-check, the code assumes the wakeup itself is proof. That assumption is too weak.
A loop makes the condition itself the authority.
That is the correct mindset.
Failure Modes
Using if instead of while leads to:
- removing from an empty queue
- proceeding on the wrong condition
- rare race bugs that are hard to reproduce
- hidden fragility when multiple waiters exist
This mistake often passes light testing and fails only under timing pressure.
Testing and Debugging Notes
When reviewing low-level coordination code:
- find every
wait() - check whether it is inside a condition loop
- verify the condition and state are protected by the same monitor
If a wait() is guarded by if, assume the code is wrong until proven otherwise.
That is a good review default.
Decision Guide
Always use:
while (!condition) {
wait();
}
Never use:
if (!condition) {
wait();
}
There are many nuanced concurrency rules. This one is not nuanced.
Key Takeaways
- waking up is not proof that the condition is safe
wait()must be paired with loop-based re-checking- condition state, monitor ownership, and re-checking must all line up