Java Tutorial
🔍

wait, notify & notifyAll

wait, notify & notifyAll

Object.wait(), notify(), and notifyAll() are the low-level building blocks Java has offered since its very first release for one thread to pause until another signals it — the foundation the classic producer-consumer pattern is built on, and still worth understanding even though java.util.concurrent now offers higher-level alternatives for most everyday cases.

What Is wait/notify?

wait(), notify(), and notifyAll() are methods on Object itself, not on Thread — every object in Java carries an intrinsic monitor, and these three methods let threads coordinate through it: one thread pauses on the monitor with wait(), and another thread wakes it with notify() or notifyAll() once there is something to do.

wait(), notify(), and notifyAll() must all be called from inside a synchronized block on the exact object whose monitor is being used — calling any of them without holding that monitor throws IllegalMonitorStateException immediately.

Why Thread Safety Matters Here

Coordinating threads without wait()/notify() usually means polling a flag in a loop, which technically works but burns CPU the entire time nothing has actually changed — on a busy server, a handful of threads doing this can measurably affect throughput for everything else running on the same cores. Getting the coordination itself wrong is worse: a missed signal, or a wait() woken for the wrong reason, can leave a thread parked forever waiting for a notification that already happened before it started listening.

How It Works

One sentence before the diagram: this shows the handoff between a producer and a consumer through the shared monitor.

Producer thread                 Monitor (LOCK)              Consumer thread
                                                          synchronized(LOCK)
                                                          condition false
                                                          call wait() -> releases LOCK
synchronized(LOCK) acquired
set data, dataReady = true
call notify()  ------------------------------------------->  wakes, blocks on
release LOCK (exit block)                                    re-acquiring LOCK
                                                          re-acquires LOCK
                                                          re-check condition -> true
                                                          exit wait loop, use data

Checking a flag in a tight loop works, but it burns CPU the entire time a thread is waiting, checking the same condition over and over as fast as it possibly can.

1// File: BeforeWaitNotify.java 2 3public class BeforeWaitNotify { 4 5 static volatile boolean dataReady = false; 6 static String data; 7 8 public static void main(String[] args) throws InterruptedException { 9 Thread producer = new Thread(() -> { 10 data = "Order-4021"; 11 dataReady = true; 12 }); 13 14 Thread consumer = new Thread(() -> { 15 while (!dataReady) { 16 // busy-waiting - burns CPU checking the flag repeatedly 17 } 18 System.out.println("Consumed: " + data); 19 }); 20 21 consumer.start(); 22 Thread.sleep(50); 23 producer.start(); 24 25 consumer.join(); 26 producer.join(); 27 } 28}
Output:
Consumed: Order-4021

wait() parks the consumer thread without spinning at all, releasing its lock while paused, and notify() wakes it the moment there is actually something to do.

1// File: AfterWaitNotify.java 2 3public class AfterWaitNotify { 4 5 static final Object LOCK = new Object(); 6 static boolean dataReady = false; 7 static String data; 8 9 public static void main(String[] args) throws InterruptedException { 10 Thread producer = new Thread(() -> { 11 synchronized (LOCK) { 12 data = "Order-4021"; 13 dataReady = true; 14 LOCK.notify(); 15 } 16 }); 17 18 Thread consumer = new Thread(() -> { 19 synchronized (LOCK) { 20 while (!dataReady) { 21 try { 22 LOCK.wait(); 23 } catch (InterruptedException e) { 24 Thread.currentThread().interrupt(); 25 } 26 } 27 } 28 System.out.println("Consumed: " + data); 29 }); 30 31 consumer.start(); 32 Thread.sleep(50); 33 producer.start(); 34 35 consumer.join(); 36 producer.join(); 37 } 38}
Output:
Consumed: Order-4021

Both reach the same result, but the second version never wastes a single CPU cycle spinning — the consumer thread is genuinely parked, not polling, for the entire time it waits. Calling any of the three methods without holding the relevant monitor fails immediately rather than silently:

1// File: IllegalMonitorStateMistakeDemo.java 2 3public class IllegalMonitorStateMistakeDemo { 4 public static void main(String[] args) { 5 Object lock = new Object(); 6 try { 7 lock.wait(); 8 } catch (InterruptedException e) { 9 Thread.currentThread().interrupt(); 10 } catch (IllegalMonitorStateException e) { 11 System.out.println("Caught: " + e.getClass().getSimpleName()); 12 } 13 } 14}
Output:
Caught: IllegalMonitorStateException

notify() wakes exactly one arbitrarily-chosen waiting thread; notifyAll() wakes every thread waiting on that monitor, letting each one re-check its own condition once it reacquires the lock.

Code Examples

The classic producer-consumer pattern is the primary use case — one or more threads produce data and one or more threads consume it, covered in full in the real-world example below. Beyond that, wait()/notify() also cover signaling that a shared resource has become available, such as a connection pool releasing an entry back for a waiting thread to claim, coordinating a fixed sequence of steps across threads, where one thread must complete a phase before another can begin its own, and a worker pool waiting for work to arrive, where notifyAll() wakes every idle worker so whichever reacquires the lock first claims the next task.

Real-World Example

A print shop queues incoming jobs on a producer thread and prints them in order on a consumer thread, using wait() and notifyAll() to coordinate a queue instead of a single value.

1// File: PrintJobQueue.java 2import java.util.*; 3 4public class PrintJobQueue { 5 private final Queue<String> jobs = new LinkedList<>(); 6 private boolean finished = false; 7 8 public synchronized void submit(String job) { 9 jobs.add(job); 10 notifyAll(); 11 } 12 13 public synchronized void finishSubmitting() { 14 finished = true; 15 notifyAll(); 16 } 17 18 public synchronized String takeNext() throws InterruptedException { 19 while (jobs.isEmpty() && !finished) { 20 wait(); 21 } 22 return jobs.poll(); 23 } 24}
1// File: PrintShopDemo.java 2import java.util.*; 3 4public class PrintShopDemo { 5 public static void main(String[] args) throws InterruptedException { 6 PrintJobQueue queue = new PrintJobQueue(); 7 List<String> printed = Collections.synchronizedList(new ArrayList<>()); 8 9 Thread consumer = new Thread(() -> { 10 try { 11 String job; 12 while ((job = queue.takeNext()) != null) { 13 printed.add(job); 14 } 15 } catch (InterruptedException e) { 16 Thread.currentThread().interrupt(); 17 } 18 }); 19 20 consumer.start(); 21 22 Thread producer = new Thread(() -> { 23 queue.submit("Invoice-101"); 24 queue.submit("Invoice-102"); 25 queue.submit("Invoice-103"); 26 queue.finishSubmitting(); 27 }); 28 29 producer.start(); 30 producer.join(); 31 consumer.join(); 32 33 System.out.println("Printed jobs: " + printed); 34 } 35}
Output:
Printed jobs: [Invoice-101, Invoice-102, Invoice-103]

Regardless of exactly how the producer and consumer threads happen to be scheduled relative to each other, the three jobs are always printed in the order they were submitted — takeNext()'s while loop correctly waits through any moment the queue happens to be temporarily empty, and finishSubmitting()'s notifyAll() is what lets the consumer's loop eventually exit cleanly with null. A mistake that appears often in fresher pull requests is checking wait()'s condition with an if statement instead of a while loop, trusting that a single wakeup always means the condition is now true. A spurious wakeup — the JVM waking a thread with no notify() actually intended for it, a documented possibility — or another consumer thread draining the queue first, both mean the condition needs to be re-checked, exactly why takeNext() here loops on while (jobs.isEmpty() && !finished) instead of testing it only once.

Best Practices

Always call wait() inside a while loop that re-checks the actual condition, never inside an if, to guard against both spurious wakeups and another thread having already consumed whatever became available.

Prefer notifyAll() over notify() whenever more than one thread might be waiting on the same monitor for different reasons — notify()'s choice of which thread to wake is unspecified, and can wake the wrong one.

Reach for java.util.concurrent.BlockingQueue instead of hand-writing a producer-consumer with wait() and notify() for new code — it solves the same problem this article's PrintJobQueue hand-writes, with far less room for error, and ExecutorService is the more common way to distribute work across multiple consumer threads today.

Keep the code inside a synchronized block calling wait() as small and focused as possible, limited to the condition check and the wait itself, so the lock is held for the shortest time necessary.

Common Mistakes

Calling wait(), notify(), or notifyAll() without holding the relevant object's monitor throws IllegalMonitorStateException, exactly as demonstrated in this article's IllegalMonitorStateMistakeDemo example above.

Using notify() instead of notifyAll() when multiple threads are waiting on the same monitor for different conditions can wake the wrong thread, leaving a genuinely eligible thread waiting indefinitely while an ineligible one wakes up, finds its own condition still false, and simply waits again.

1// Illustrative only - if two different consumer threads are waiting on the 2// same monitor for two different conditions, notify() might wake the one 3// whose condition still is not met, leaving the other one waiting 4// indefinitely even though its own condition just became true. 5public class NotifyWrongThreadMistake { 6 static final Object LOCK = new Object(); 7 static boolean conditionA = false; 8 static boolean conditionB = false; 9 10 static void waitForA() throws InterruptedException { 11 synchronized (LOCK) { 12 while (!conditionA) { 13 LOCK.wait(); 14 } 15 } 16 } 17 18 static void waitForB() throws InterruptedException { 19 synchronized (LOCK) { 20 while (!conditionB) { 21 LOCK.wait(); 22 } 23 } 24 } 25 26 static void signalB() { 27 synchronized (LOCK) { 28 conditionB = true; 29 LOCK.notify(); // might wake the thread waiting for A instead 30 } 31 } 32}

Interview Questions

Q1. Why must wait(), notify(), and notifyAll() be called from within a synchronized block?

Because all three operate on an object's intrinsic monitor, and the calling thread must already own that monitor for the operation to be meaningful — calling any of them without holding it throws IllegalMonitorStateException. Interviewers listen for whether you connect this to the monitor specifically, not just recite it as a syntax rule.

Q2. What is the difference between notify() and notifyAll()?

notify() wakes exactly one arbitrarily-chosen thread waiting on the monitor. notifyAll() wakes every waiting thread, letting each one reacquire the lock in turn and re-check its own condition — the safer default whenever more than one thread might be waiting for different reasons. The nuance being tested is whether you know when notify() is actually safe to use, not just that notifyAll() exists.

Q3. Why should wait() always be called inside a while loop rather than an if statement?

Because a thread can wake from wait() without its condition actually being true — through a spurious wakeup, or because another thread already consumed whatever became available — a while loop re-checks the condition and goes back to waiting if it still does not hold; an if would not. This is one of the most frequently asked questions on this topic at product companies specifically, since it exposes whether a candidate has actually written this code before.

Q4. What happens to a lock when a thread calls wait() on it?

The thread releases the lock while it waits, allowing other threads to acquire it and make progress — this is the key difference from Thread.sleep(), which holds any locks the thread currently owns for the entire duration of the sleep. Interviewers frequently pair this with a follow-up asking why sleep() would be wrong for the same job.

Q5. What is a spurious wakeup?

A documented possibility where a thread waiting via wait() can wake up without any corresponding notify() or notifyAll() call having actually targeted it — precisely why the condition must always be re-checked in a loop rather than assumed true after waking. The nuance interviewers want is recognizing this as a documented JVM behavior, not a bug in application code.

Q6. What is the classic use case for wait/notify, and what modern alternative often replaces it?

The producer-consumer pattern, demonstrated in this article's print shop example, is the classic use case. java.util.concurrent.BlockingQueue is the modern alternative that solves the same problem directly, without requiring any hand-written wait()/notifyAll() logic. Naming the modern alternative unprompted is usually what separates a strong answer from an adequate one here.

Q7. What exception is thrown if wait() or notify() is called without holding the object's monitor?

IllegalMonitorStateException, an unchecked exception, thrown immediately at the point of the call — exactly as demonstrated in this article's Common Mistakes section. Service-company interviewers commonly ask for the exact exception name here as a quick recall check.

FAQs

Do wait() and notify() need to be called on the same object that the synchronized block locks on?

Yes. Calling wait() or notify() on a different object than the one the current thread is synchronized on throws IllegalMonitorStateException, since the thread does not own that other object's monitor.

Can wait() be called with a timeout?

Yes, wait(long timeoutMillis) waits at most the given duration before waking on its own even without a notify(), which is precisely why a thread waking from a timed wait() also needs its condition re-checked in a loop.

Does notifyAll() wake threads waiting on a different object's monitor?

No. notifyAll() only wakes threads currently waiting on the exact same object's monitor it was called on — it has no effect on threads waiting elsewhere.

Is BlockingQueue a modern replacement for hand-written wait/notify producer-consumer code?

Yes, for the vast majority of everyday cases — LinkedBlockingQueue, ArrayBlockingQueue, and the rest of the BlockingQueue implementations handle the same coordination this article's PrintJobQueue hand-writes, tested and correct, with none of the manual bookkeeping.

Does calling notify() with no thread currently waiting cause an error?

No, it is a harmless no-op — if no thread is currently waiting on the monitor, notify() and notifyAll() simply have nothing to wake and do nothing further.

Can Thread.sleep() be used instead of wait() for this kind of coordination?

Not safely for the same purpose — sleep() does not release any locks the thread holds, and it has no mechanism to be woken early by another thread's signal, making it unsuitable for the kind of lock-releasing, event-driven coordination wait()/notify() provide.

Does wait() release the lock while the thread is paused?

Yes, entirely — this is what allows another thread to acquire the same lock, make the change the waiting thread cares about, and call notify() or notifyAll() to wake it, all while the original thread's lock is released.

Summary

wait(), notify(), and notifyAll() let one thread pause without spinning and another thread wake it exactly when there is something to do, forming the foundation of the producer-consumer pattern — all three require holding the relevant object's monitor, and wait() must always be checked in a while loop to guard against spurious wakeups and stale conditions.

The habit worth carrying forward from this article's print shop example is reaching for java.util.concurrent.BlockingQueue in new code rather than hand-writing this coordination, and, whenever wait()/notify() genuinely are the right tool, always preferring notifyAll() unless there is a specific, well-understood reason every waiting thread shares exactly one condition.

What to Read Next