Hook: These methods are the “knock, unlock, and wake everyone in the room” tools of Java concurrency — and interviewers love them because one small mistake can freeze an entire app.
Question: What are wait(), notify(), and notifyAll() in Java?
Answer: They are methods on Object used for thread coordination through an object's monitor, which is the lock tied to synchronized. wait() makes the current thread release that lock and sleep until another thread signals it; notify() wakes one waiting thread; notifyAll() wakes all waiting threads. They must be called while holding the same object's monitor, or Java throws IllegalMonitorStateException.
Interview-Ready Answer: I use wait(), notify(), and notifyAll() for low-level thread coordination on an object's monitor. wait() releases the lock and pauses the thread until it is signaled, while notify() wakes one waiting thread and notifyAll() wakes every waiting thread on that monitor. The key detail is that all of them must be called inside a synchronized block on the same object, and in real code I usually guard wait() with a loop because spurious wakeups can happen.
Detailed Explanation: Think of every Java object as having a built-in waiting room called a monitor (the lock used by synchronized). These methods are not general thread commands; they are ways for threads to cooperate using that monitor. The big idea is simple: one thread waits until some condition becomes true, and another thread changes the condition and signals it.
synchronized(obj) block and checks a condition, such as “is the queue empty?”obj.wait(). This does two things at once: it releases obj's monitor and parks the thread in WAITING state.notify() or notifyAll().That re-check is essential because a thread can wake up for reasons other than your notification, and because another thread may have grabbed the condition first.
wait() must be in a loopwait() can return even if nobody intended it to proceed. This is called a spurious wakeup, which means “it woke up without a real signal.” Also, even after a real wakeup, the condition may no longer be true by the time the thread gets the lock again. The safe pattern is:
while loop.wait() if the condition is not satisfied.notify() vs notifyAll()notify() chooses one arbitrary waiting thread on that monitor. You do not get to pick which one. That is why notify() is easy to use incorrectly when different threads are waiting for different conditions. notifyAll() wakes every waiting thread; they race to reacquire the lock, then each thread re-checks the condition and only the correct one proceeds.
| Method | What it does | Best use | Risk |
|---|---|---|---|
wait() | Releases lock and blocks | Pause until condition changes | Must loop; can deadlock if never signaled |
notify() | Wakes one waiter | Single condition, one kind of waiter | Wrong thread may wake |
notifyAll() | Wakes all waiters | Multiple conditions or mixed waiters | More context switching |
BlockingQueue, CountDownLatch, Semaphore, and Lock/Condition are usually safer.Performance note: signaling itself is roughly O(1), but notifyAll() can cause a “thundering herd” where many threads wake up, fight for the lock, and go back to sleep. That extra contention can be expensive under load, especially with dozens or hundreds of waiters.
IllegalMonitorStateException: thrown if you call wait(), notify(), or notifyAll() without owning the monitor.synchronized, another thread can change it before you wait, and you may sleep forever.wait() releases only the monitor of the object you call it on, not any other locks you hold.wait() throws InterruptedException, so the waiting code must handle cancellation cleanly.Memory Hook: “wait = leave the room, notify = tap one shoulder, notifyAll = shout to everyone — but nobody moves until they get the key back.”
Real-World Example: Imagine a checkout service in an e-commerce app where worker threads pull orders from a shared buffer. When the buffer is empty, consumers call wait() and stop burning CPU. When a producer thread receives a new order from the API, it adds the order, updates the shared state, and calls notifyAll() so any waiting consumer can wake up and compete for the item.
What goes wrong if you misunderstand it: A team uses notify() while multiple consumer types are waiting on the same monitor, such as “normal orders” and “priority orders.” The wrong thread wakes repeatedly, finds its condition still false, and goes back to waiting while the right thread stays asleep. In production, logs show threads stuck in WAITING, orders pile up, latency climbs, and autoscaling does not help because the bug is not lack of threads — it is bad signaling.
import java.util.LinkedList;
import java.util.List;
public class WaitNotifyDemo {
public static void main(String[] args) throws InterruptedException {
BoundedBuffer buffer = new BoundedBuffer(1);
Thread consumer1 = new Thread(() -> consume(buffer, "C1"), "Consumer-1");
Thread consumer2 = new Thread(() -> consume(buffer, "C2"), "Consumer-2");
Thread producer = new Thread(() -> produce(buffer), "Producer");
consumer1.start();
consumer2.start();
// Give consumers time to reach wait(). This is only for demo clarity.
Thread.sleep(300);
producer.start();
producer.join();
consumer1.join();
consumer2.join();
System.out.println("Demo finished.");
// Edge case demo: calling wait/notify without owning the monitor.
tryIllegalMonitorState();
}
private static void consume(BoundedBuffer buffer, String name) {
try {
Integer value = buffer.take(name);
System.out.println(name + " consumed: " + value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(name + " was interrupted while waiting.");
}
}
private static void produce(BoundedBuffer buffer) {
try {
buffer.put(42);
System.out.println("Producer added: 42");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Producer interrupted.");
}
}
private static void tryIllegalMonitorState() {
Object lock = new Object();
try {
// This is a deliberate failure path: the current thread does not own 'lock'.
lock.wait(50);
} catch (IllegalMonitorStateException e) {
System.out.println("Expected failure: " + e.getClass().getSimpleName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static class BoundedBuffer {
private final List<Integer> items = new LinkedList<>();
private final int capacity;
BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public synchronized void put(int value) throws InterruptedException {
while (items.size() == capacity) {
// Always use while: a wakeup does not guarantee the condition is truly safe.
wait();
}
items.add(value);
// notifyAll is safer when multiple threads may be waiting for different reasons.
notifyAll();
}
public synchronized Integer take(String consumerName) throws InterruptedException {
while (items.isEmpty()) {
System.out.println(consumerName + " is waiting because the buffer is empty.");
wait();
}
Integer value = items.remove(0);
// Wake producers/other consumers that may now make progress.
notifyAll();
return value;
}
}
}
Follow-up & Tricky Questions:
wait() be inside a while loop? Because wakeups can be spurious, and because another thread may change the condition before you regain the lock. The loop re-checks the real state, which is the only safe source of truth.synchronized, wait(), and notify(). People often say “lock” casually, but the exact wait/notify mechanism is tied to the object monitor.notify() sometimes cause bugs while notifyAll() works? notify() wakes one arbitrary waiter, which may be a thread whose condition is still false. notifyAll() wakes everyone so each thread can re-check its own condition, which is safer when multiple conditions share the same monitor.wait().wait() release all locks the thread holds? No. It releases only the monitor of the object it is waiting on. Any other locks the thread holds remain held, which can cause deadlocks if you are not careful.Thread.sleep() instead? sleep() just pauses a thread for time; it does not release a lock and it does not coordinate with another thread. wait() is about condition-based communication.notifyAll() always better? Not always. It is safer, but it can wake many unnecessary threads and create contention. In simple single-condition designs, notify() can be fine if you can prove only one kind of waiter exists.wait() belong to Thread? No, and this is a classic trap. It belongs to Object because the coordination is tied to the monitor of the shared object, not to the thread itself.notify() choose the longest-waiting thread? No guarantee is given. The JVM decides which waiting thread to wake, so you should never rely on ordering.Common Mistakes:
wait() or notify() outside synchronized: this throws IllegalMonitorStateException. Correction: always own the same monitor before calling them.if instead of while: a thread may wake up too early or the condition may change before it runs. Correction: re-check the condition in a loop.notify() when multiple conditions share one monitor: the wrong waiter may wake and make no progress. Correction: prefer notifyAll() unless you can prove single-condition waiting.Memory Hook: “Wait = let go, Notify = tap one, NotifyAll = wake the room, and always re-check the door before entering.”
Cheat Sheet:
wait() releases the object's monitor and blocks.notify() wakes one arbitrary waiting thread.notifyAll() wakes all waiting threads on that monitor.synchronized.while, not if, around wait().Practice Tasks:
notify() instead of notifyAll() and observe why it becomes risky with multiple consumers.wait() and log when a consumer gives up waiting.BlockingQueue and compare how much simpler it becomes.