I Thought I Understood wait() — Until My Thread Woke Up Without a notify()

In Java concurrency, one rule pops up repeatedly:
Always wrap
wait()
in awhile
loop, not anif
statement.
But why? What’s wrong with using if
? After all, if the condition isn’t met, we wait — and if we’re notified, we proceed. Sounds simple, right?
Well, it turns out this advice is grounded in how threads work at the deepest levels of the JVM and the underlying OS. Let’s explore this rule from first principles and see why spurious wakeups make while
not just better — but essential.
What Actually Happens When a Thread Calls wait()
?
Let’s walk through what Java’s Object.wait()
actually does:
The current thread must hold the lock (monitor) on the object.
It then releases that lock and enters a waiting state.
It remains inactive — not using CPU — until it’s either:
Notified via
notify()
ornotifyAll()
, orInterrupted, or...
👻 Woken up for no reason (we'll get to that).
👻 Enter the Spurious Wakeup
Here’s the kicker: the Java Virtual Machine explicitly allows a thread to wake up from wait()
even when no one notified it.
This unexpected behavior is called a spurious wakeup.
⚠️ Yes, it’s real — and it’s allowed by the Java Language Specification.
A thread can resume from
wait()
without being notified, due to internal optimizations in the JVM or OS-level thread scheduler.
Why would JVMs allow this?
🔍 Why Spurious Wakeups Happen
Let’s drop down into system-level behavior.
The role of the JVM and OS:
Modern JVMs rely on the operating system for scheduling and managing threads.
Operating systems often make aggressive optimizations for performance or fairness.
As a result, sometimes a thread that’s waiting is told to wake up and recheck its condition — even if no other thread signaled it.
This avoids edge-case deadlocks or starvation in complex scheduling situations.
It’s a trade-off: performance and safety vs. strict predictability.
🔁 Why while
Fixes the Problem
Imagine this code:
synchronized (lock) {
if (!condition) {
lock.wait(); // Might wake up spuriously!
}
// Assumes condition is true — but it might not be!
}
If the thread wakes up spuriously, it skips the wait()
and executes code under a false assumption — the condition isn’t actually true.
✅ The correct pattern:
synchronized (lock) {
while (!condition) {
lock.wait(); // Re-check condition every time
}
// Now it’s safe to proceed
}
This ensures the thread verifies the condition every time it wakes up, whether it was a real notification or a ghostly spurious wakeup.
Analogy: The Guard and the Door
Picture a security guard who only opens a door if someone rings the bell (notify()
).
But sometimes, they randomly wake up and think, “Did someone ring? Better check.” That’s a spurious wakeup.
If the guard blindly opens the door every time they wake up — that’s if
.
If they check first and only open if the bell truly rang — that’s while
.
🪛 A Simple Correct Example
class SharedResource {
private boolean ready = false;
public synchronized void waitUntilReady() throws InterruptedException {
while (!ready) {
wait(); // Keeps checking until ready is really true
}
// Safe to proceed
}
public synchronized void markReady() {
ready = true;
notifyAll();
}
}
Even if wait()
wakes up spuriously, the loop ensures it doesn’t proceed until the shared state is actually valid.
✅ Summary
Concept | Why It Matters |
Spurious Wakeup | Threads can wake up without notify() |
Why It Happens | JVM/OS-level optimizations and fairness |
Why Use while | It re-checks the condition on each wake-up |
Why Not if | if assumes condition is true after wait() |
🏁 Final Thought
Concurrency is hard. But when you understand these principles from the ground up — how JVMs, locks, and threads actually work — the seemingly arbitrary rules start to make perfect sense.
So next time you're synchronizing threads in Java, remember:
while (!condition) wait();
— it's not just safer. It's correct.
Subscribe to my newsletter
Read articles from Vijay directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
