wait(), notify() and notifyAll()
How threads sleep on a monitor until a condition changes, the three rules that keep wait/notify correct, and why a single notify() can hang a producer-consumer.
On this page
- The problem: waiting for a condition
- Picture it first: the pickup counter
- Under the hood: wait() step by step
- Rule 1: hold that object's monitor
- Rule 2: always wait in a while loop, never an if
- Rule 3: change the state before you notify, under the same lock
- The guarded-wait template
- notify() vs notifyAll(): the hang you can reproduce
- Timeouts and interrupts
- wait() vs sleep(), in one table
- Why on Object, and which objects not to use
- What to use instead (and my default)
- Real world
- Advanced: what HotSpot actually does
- How to say it in the interview
🔔 What are wait(), notify() and notifyAll()?
They're how one Java thread says "I can't continue until something changes, wake me when it does", and how another thread says "it changed". wait() puts the calling thread to sleep and hands back the lock it was holding. notify() wakes one sleeping thread; notifyAll() wakes all of them. Think of a customer at a ramen shop who sits down on a bench until the cook rings the bell: no pacing, no asking "is it ready yet?" every second.
For this lesson, we'll build one small thing: a pickup counter with room for a single order, where customers wait for food and a cook serves it. That tiny example carries every rule you need, every bug people actually ship, and every question interviewers ask. Interviewers love this topic because the code is 10 lines long and almost everyone writes one of those lines wrong.
Below the line (out of scope)
- The lock itself (
synchronized, what a monitor is): synchronized and monitors - The full bounded buffer with many slots: The producer-consumer problem
- The long story of why the language forces
synchronizedhere: why wait/notify need synchronized ReentrantLockandConditionin depth get their own lesson ("ReentrantLock and Condition")
The 30-second answer
wait(), notify() and notifyAll() live on Object because every object has a monitor: a lock plus a wait set. You must own that object's monitor (be inside synchronized on it) or you get IllegalMonitorStateException. wait() atomically releases the monitor and parks the thread in the wait set; when woken, it re-acquires the monitor before returning. notify() moves one arbitrary waiter out, notifyAll() moves all of them, and the notifier keeps the lock until it leaves its block. Three rules: hold the lock, always re-check the condition in a while loop (spurious wake-ups and stolen conditions), and change the state before notifying, under the same lock. Prefer notifyAll() unless all waiters are interchangeable. In new code, reach for BlockingQueue, CountDownLatch or Condition first.
What you’ll learn
- Explain a monitor's entry set (
BLOCKED) and wait set (WAITING) with the pickup-counter picture - Walk through what
wait()does: atomically release and park, then re-acquire the monitor before returning - Apply the three rules: own the monitor or get
IllegalMonitorStateException, **wait in awhileloop**, change state before notifying - Write the guarded-wait template from memory
- Reproduce and explain the **
notify()hang** with mixed waiters, and say whennotify()is safe - Handle
wait(timeout)deadlines andInterruptedExceptioncorrectly, and contrastwait()withsleep() - Pick a modern replacement:
BlockingQueue,CountDownLatch,CompletableFutureorCondition - Describe HotSpot's
ObjectMonitorwait set and how virtual threads behave inObject.wait()on Java 21 vs 24
Interview radar
Asked very often: Why must wait() be called inside synchronized? Why while and not if?
Core follow-up: notify() vs notifyAll(), and wait() vs sleep().
Depth check: Why are these methods on Object and not on Thread?
01The problem: waiting for a condition
Here's the situation. A customer thread needs an order, and the order isn't ready. The customer can't do anything useful until a condition becomes true: "there's food on the counter".
The first thing most people write is a loop that keeps checking:
while (order == null) {
Thread.sleep(50); // "is it ready yet?" 20 times a second
}
This works, badly. With sleep(50) the customer notices the food up to 50 ms late, and with 1,000 customers you're doing 20,000 pointless checks a second. Drop the sleep and it's worse: a bare while (order == null) { } spins one CPU core at 100% doing nothing. (It's also broken for a sneakier reason: without a lock, the customer may never see the cook's write. That's the planned "Visibility and reordering" lesson.)
What we want is the restaurant version: sit down, stop using CPU, and get woken exactly when something changes. That's what wait() and notify() give you.
02Picture it first: the pickup counter
Let's set up the restaurant, because it maps one-to-one onto Java. The counter has one key. Only the person holding the key may look at the counter or touch the order slot. That key is the object's lock, and the counter plus its key is the monitor (the lock every Java object carries, plus its waiting areas).
There are two places to wait, and this is the bit that trips people up:
- The door queue is for people who want the key. In Java this is the entry set, and those threads are
BLOCKED. - The bench is for people who looked, saw no food, and gave the key back. That's the wait set, and those threads are
WAITING.
When the cook rings the bell (notify()), someone gets up from the bench. But they don't walk straight to the food. They join the door queue and wait for the key like everyone else. Here's the whole picture:
The pickup counter: one key, a door queue and a bench
That last arrow explains half the rules below. Being woken is not the same as being served.
03Under the hood: wait() step by step
Try it yourself first
Before reading on: a customer is inside wait(). The cook calls notify() and then spends another second wiping the counter before leaving synchronized. What state is the customer in during that second, and can it run?
Here's what wait() does, in order:
- You must already own the monitor. You're inside
synchronized (counter). - You check the condition and it's false: no order.
wait()releases the monitor and joins the wait set in one atomic step. There's no gap where you've let go of the key but aren't on the bench yet. You're nowWAITINGand using no CPU.- Another thread changes the state and calls
notify(). You're moved from the wait set to the entry set: now you'reBLOCKED, queuing for the key. - The notifier keeps the lock until it leaves its
synchronizedblock. Only then can you re-acquire it. wait()returns, with the monitor held again, and your loop checks the condition one more time.
So the answer to the TRY: the customer is BLOCKED for that whole second and can't run. notify() doesn't hand over the lock; it only makes a waiter eligible to compete for it.
What wait() does, step by step
Here's the counter in code. It's the canonical shape, and we'll pick it apart rule by rule.
public class PickupCounter {
private String order; // the one slot on the counter; null = empty
public synchronized void serve(String dish) throws InterruptedException {
while (order != null) { // counter full: cook waits
wait();
}
order = dish; // 1. change the state...
log("cook puts " + dish + " on the counter");
notifyAll(); // 2. ...then ring the bell
}
public synchronized String collect() throws InterruptedException {
while (order == null) { // nothing ready: customer sits on the bench
log("counter empty, waiting");
wait(); // releases the lock while waiting
log("woke up, checking again");
}
String dish = order;
order = null;
notifyAll(); // the slot is free again
return dish;
}
static void log(String msg) {
System.out.printf("%-9s %s%n", Thread.currentThread().getName(), msg);
}
public static void main(String[] args) throws Exception {
PickupCounter counter = new PickupCounter();
Thread customer = new Thread(() -> {
try {
for (int i = 0; i < 2; i++) log("got " + counter.collect());
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "customer");
customer.start();
Thread.sleep(100); // let the customer arrive first
Thread cook = new Thread(() -> {
try {
counter.serve("ramen");
Thread.sleep(100);
counter.serve("gyoza");
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "cook");
cook.start();
customer.join();
cook.join();
}
}
Running java PickupCounter.java on Java 21 and 26 printed this every time:
customer counter empty, waiting
cook cook puts ramen on the counter
customer woke up, checking again
customer got ramen
customer counter empty, waiting
cook cook puts gyoza on the counter
customer woke up, checking again
customer got gyoza
Notice three things. The cook could enter serve() at all, which proves the waiting customer had given the lock back. The customer's "woke up" line always comes after the cook's line, because it can't run until the cook leaves serve(). And the customer says "checking again": it never trusts the wake-up, it trusts the state.
04Rule 1: hold that object's monitor
You can only call wait(), notify() or notifyAll() on an object whose monitor you currently own. Not a lock: that object's lock. Here's what happens otherwise:
public class NoLock {
public static void main(String[] args) throws InterruptedException {
Object counter = new Object();
try {
counter.notify(); // not inside synchronized (counter)
} catch (IllegalMonitorStateException e) {
System.out.println("notify() -> " + e);
}
try {
counter.wait();
} catch (IllegalMonitorStateException e) {
System.out.println("wait() -> " + e);
}
Object other = new Object();
synchronized (other) { // holding the WRONG object's lock
try {
counter.wait();
} catch (IllegalMonitorStateException e) {
System.out.println("wrong lock -> " + e.getClass().getSimpleName());
}
}
}
}
notify() -> java.lang.IllegalMonitorStateException: current thread is not owner
wait() -> java.lang.IllegalMonitorStateException: current thread is not owner
wrong lock -> IllegalMonitorStateException
This isn't Java being fussy. The condition (order == null) is shared state, so checking it and going to sleep must happen under the lock that protects it. Otherwise the cook could serve and ring in the tiny gap between your check and your wait(). The runtime check makes the safe version the only version that compiles into working code. The full history is in why wait/notify need synchronized.
05Rule 2: always wait in a while loop, never an if
You might be thinking: "I was woken because the food arrived, so why check again?" Because a wake-up is a hint, not a promise. Two things can make it false:
- Stolen conditions.
notifyAll()wakes two customers, but there's one order. Customer A gets the key first and takes it. When B finally gets the key, the slot is empty again. - Spurious wake-ups. JLS §17.2.1 explicitly allows a thread to return from
wait()with nobody callingnotify(). It's rare, it's real, and POSIX condition variables have the same rule.
Here's the stolen-condition case on the board:
Why while, not if: two customers woken, one order
if, customer B walks off with null. With while, B re-checks and goes back to the bench.The while loop costs one extra comparison per wake-up. Leaving it out costs a bug that shows up once a week under load. Even the Object.wait Javadoc says to always call it in a loop.
06Rule 3: change the state before you notify, under the same lock
notify() doesn't queue up. If nobody is on the bench when the bell rings, the ring is gone. That's called a lost wake-up: the waiter goes to sleep after the only notification it was ever going to get.
The lost wake-up: the bell rang before anyone sat down
The fix isn't "ring louder". It's to make the bell a reminder about state that anyone can check. The cook sets order = dish and then calls notifyAll(), both inside the same synchronized block. The customer checks order under that same lock before deciding to wait. Now either the customer checks first and is safely on the bench before the cook can ring, or the cook goes first and the customer sees the order and never sits down. No gap.
07The guarded-wait template
Put the three rules together and you get the one shape you should be able to write on a whiteboard without thinking:
// waiting side
synchronized (lock) {
while (!condition) { // rule 2: loop, re-check after every wake-up
lock.wait(); // rule 1: we own lock's monitor
}
// condition is true AND we hold the lock: act on it
}
// changing side
synchronized (lock) { // same lock
changeTheState(); // rule 3: state first...
lock.notifyAll(); // ...then notify
}
The condition is called a condition predicate, and the whole thing is a guarded wait. When I review code with wait() in it, I'm looking for exactly this skeleton. Anything else needs a very good reason.
08notify() vs notifyAll(): the hang you can reproduce
notify() wakes one waiter, and the JVM picks which one. You don't get a say, and it isn't first-come-first-served. notifyAll() wakes all of them and lets each re-check.
notify() is cheaper, so why not always use it? Because on one monitor there's only one bench, and cooks and customers both sit on it. Cooks wait for "slot empty", customers wait for "slot full". If the bell wakes the wrong kind of thread, the right kind may sleep forever.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Timeouts and interrupts
- wait() vs sleep(), in one table
- Why on Object, and which objects not to use
- What to use instead (and my default)
- Real world
- Advanced: what HotSpot actually does
- How to say it in the interview
From ₹299 · one-time payment · No auto-renewal
Further reading (13)
- Object (Java SE 21 API): wait, notify, notifyAll: must own the monitor, IllegalMonitorStateException, wait in a loop, wait(0) waits forever, arbitrary choice for notify
- Java Language Specification (Java SE 21) §17.2: Wait Sets and Notification: §17.2.1 wait and spurious wake-ups, §17.2.2 notification, §17.2.4 interactions of waits, notification and interruption
- Java Language Specification (Java SE 21) §17.1: Synchronization: every object has a monitor
- Thread (Java SE 21 API): join: join loops on this.wait; notifyAll on termination; do not use wait/notify on Thread instances
- Condition (Java SE 21 API): the notFull/notEmpty bounded buffer example; spurious wake-ups
- POSIX: pthread_cond_wait: spurious wake-ups may occur; predicates must be re-evaluated in a loop
- JDK-6822370: ReentrantReadWriteLock: threads hung when there are no threads holding onto the lock: a lost unpark in HotSpot; threads parked in LinkedBlockingQueue on a lock nobody owned; -XX:+UseMembar avoided it
- OpenJDK jdk21u: objectMonitor.hpp: _WaitSet of ObjectWaiter nodes, _cxq and _EntryList
- OpenJDK wiki: Synchronization: lightweight locking vs inflated monitors in HotSpot
- JEP 444: Virtual Threads: Java 21 pinning inside synchronized; Object.wait() captures the carrier and the scheduler compensates
- JEP 491: Synchronize Virtual Threads without Pinning: Java 24: virtual threads unmount when blocking on a monitor and in Object.wait()
- Java Concurrency in Practice (Goetz et al., 2006): §14.2 condition queues, the while-loop template, missed signals, conditions for single notify
- Effective Java, 3rd edition (Bloch, 2018): Item 81: prefer concurrency utilities to wait and notify