Course contents

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.

PremiumMust know for interviewsLesson22 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. The problem: waiting for a condition
  2. Picture it first: the pickup counter
  3. Under the hood: wait() step by step
  4. Rule 1: hold that object's monitor
  5. Rule 2: always wait in a while loop, never an if
  6. Rule 3: change the state before you notify, under the same lock
  7. The guarded-wait template
  8. notify() vs notifyAll(): the hang you can reproduce
  9. Timeouts and interrupts
  10. wait() vs sleep(), in one table
  11. Why on Object, and which objects not to use
  12. What to use instead (and my default)
  13. Real world
  14. Advanced: what HotSpot actually does
  15. 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 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 a while loop**, change state before notifying
  • Write the guarded-wait template from memory
  • Reproduce and explain the **notify() hang** with mixed waiters, and say when notify() is safe
  • Handle wait(timeout) deadlines and InterruptedException correctly, and contrast wait() with sleep()
  • Pick a modern replacement: BlockingQueue, CountDownLatch, CompletableFuture or Condition
  • Describe HotSpot's ObjectMonitor wait set and how virtual threads behave in Object.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:

Fig. 1

The pickup counter: one key, a door queue and a bench

The pickup counter: one key, a door queue and a benchA restaurant pickup counter is the monitor. One customer holds the key and stands at the counter. Customers queued at the door are the entry set (BLOCKED). Customers sitting on the bench are the wait set (WAITING). When the cook rings the bell (notify), a customer leaves the bench and joins the door queue: they still have to get the key before they can look at the counter.door queue = entry setBLOCKED: want the keypickup counter(the monitor)holds the keyslot1 orderenterkitchencookbellservebench = wait setWAITING: asleep until the bellwait(): drop key, sit downbell = notify()woken: back in the queueRemember- one key, one person inside- bench people hold no key- woken != served
Two rooms, one key. The door queue (entry set) wants the key; the bench (wait set) is asleep until the bell. Notice the woken customer goes back to the queue, not to the food.

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:

  1. You must already own the monitor. You're inside synchronized (counter).
  2. You check the condition and it's false: no order.
  3. 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 now WAITING and using no CPU.
  4. Another thread changes the state and calls notify(). You're moved from the wait set to the entry set: now you're BLOCKED, queuing for the key.
  5. The notifier keeps the lock until it leaves its synchronized block. Only then can you re-acquire it.
  6. 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.

Fig. 2

What wait() does, step by step

What wait() does, step by stepSix steps. 1: the thread owns the monitor inside synchronized. 2: it checks the condition and it is false. 3: wait() releases the monitor and joins the wait set in one atomic move (WAITING). 4: another thread changes the state and calls notify, which moves the waiter to the entry set (BLOCKED). 5: when the notifier leaves synchronized, the waiter re-acquires the monitor. 6: wait() returns and the while loop checks the condition again.own the monitorinside synchronized1RUNNABLEcheck conditionorder == null? yes2RUNNABLEwait()release + sit, atomically3WAITINGbell: notify()moved to door queue4BLOCKEDget the key backafter notifier exits5BLOCKEDwait() returnsloop checks again6RUNNABLEanother thread ringswhile (!condition)Steps 3 and 4-5 are why wait() must be inside synchronized: it has a key to give back,and it never returns to your code without the key in hand.
The wait() round trip. Follow badges 1 to 6. The thread goes RUNNABLE, WAITING, BLOCKED, RUNNABLE, and it never runs your code without the key.

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 calling notify(). It's rare, it's real, and POSIX condition variables have the same rule.

Here's the stolen-condition case on the board:

Fig. 3

Why while, not if: two customers woken, one order

Why while, not if: two customers woken, one ordernotifyAll wakes customers A and B, but there is only one order. A gets the key first and takes the ramen, leaving the slot empty. B gets the key next. With if, B does not re-check and takes null, a bug. With while, B re-checks, sees the slot is empty and goes back to the bench.notifyAll()slot: ramenABwakes bothA gets the key firsttakes ramen, slot = null1B gets the key nextslot is empty now2if (slot == null) wait();B falls through, takes nullNullPointerException later,or a customer with no foodwhile (slot == null) wait();B re-checks: still emptygoes back to the benchand waits for the next order
Two woken, one order. With 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.

Fig. 4

The lost wake-up: the bell rang before anyone sat down

The lost wake-up: the bell rang before anyone sat downTwo lanes over time. Top lane, broken: the cook rings the bell at step 1 but nobody is on the bench, so the ring is lost. At step 2 the customer sits down and waits forever. Bottom lane, fixed: the cook puts the order on the counter first (state), then rings. The customer checks the counter under the same lock, sees the order and never sits down.Broken: a bell with no statecookcustomertimenotify() ding!1bench is empty: ring lostwait()2asleep foreverFixed: state first, check under the lockcookcustomertimeorder = "ramen"; notifyAll()1while (order == null) ... skip!2
The bell rang to an empty bench. Top: a notify with no state behind it is lost forever. Bottom: with the order recorded first and checked under the lock, the customer never needs the bell.

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

Practise this topicSynchronization: interview questions & scenarios
Further reading (13)