wait, notify & Coordination
wait, notify & Coordination
Mutual exclusion keeps threads from corrupting shared state, but it does not help them cooperate. Consider a classic producer-consumer pair: the consumer must not proceed until there is data to consume, and the producer must not overfill a bounded buffer. Both threads need a way to pause and resume based on a condition — that is exactly what wait, notify, and notifyAll provide.
The Guarded Block Pattern
A guarded block is the fundamental idiom: a thread checks a condition and, if the condition is not yet satisfied, suspends itself inside the monitor until another thread signals that something has changed. The sleeping thread releases the lock while it waits, so other threads can enter the monitor and make progress.
object.wait() it atomically (1) releases the monitor on object, (2) suspends itself, and (3) re-acquires the monitor before returning. This is what makes coordination possible: the waiting thread steps aside so the notifying thread can enter the same synchronized block.
The correct skeleton always uses a while loop, never an if:
if version broken: (1) spurious wakeups — the JVM is permitted to wake a waiting thread even when nobody called notify; (2) missed signals — between the moment a thread is woken and the moment it re-acquires the lock, another thread might have consumed the very item that satisfied the condition. The while loop guards against both.
notify vs notifyAll
notify() wakes exactly one thread that is waiting on the same monitor — which one is chosen by the JVM scheduler and is not predictable. notifyAll() wakes every waiting thread; each then races to re-acquire the lock, and all but one go back to waiting.
- Use
notify()when all waiting threads are waiting for the same condition and only one can proceed at a time. A pool of identical workers is the textbook example. - Use
notifyAll()when different threads wait for different conditions on the same monitor (e.g., a producer and a consumer both waiting on one object).notify()might wake the wrong thread, which goes back to sleep, leaving the ready thread stranded — a liveness failure.
notify() only after you have verified that every thread waiting on the monitor waits for exactly the same condition.
A Complete Producer-Consumer Example
The example below models a single-slot buffer: the producer puts one item in and then waits until the consumer has taken it, and vice versa.
Notice that both put and take are synchronized on this, so they share the same monitor. When the producer calls wait(), it releases the lock and the consumer can enter take(). After the consumer calls notifyAll() and exits, the producer re-acquires the lock, finds hasItem == false, and proceeds.
A minimal harness to test it:
The Three Rules You Must Never Break
- Always call wait/notify/notifyAll inside a synchronized block on the same object. Calling them outside throws
IllegalMonitorStateExceptionat runtime. - Always re-check the condition in a while loop after returning from wait. Spurious wakeups and competing threads make the
ifversion unsafe. - Call notifyAll unless you can prove notify is correct. A missed wakeup with
notify()leaves a thread suspended forever — the hardest kind of bug to reproduce.
Handling InterruptedException Correctly
wait() declares throws InterruptedException. If another thread calls interrupt() on a waiting thread, wait() throws this exception immediately. The correct response is almost always to restore the interrupted status and let the exception propagate:
Swallowing the exception silently (empty catch block) destroys the signal and makes it impossible to shut down the thread cleanly.
wait/notify vs Higher-Level Alternatives
The java.util.concurrent package introduced in Java 5 provides higher-level tools built on top of the same underlying mechanisms:
BlockingQueue(LinkedBlockingQueue,ArrayBlockingQueue) — a ready-to-use bounded or unbounded producer-consumer channel. Prefer this over hand-rolling a buffer with wait/notify.Condition(fromReentrantLock.newCondition()) — providesawait()/signal()with the same semantics as wait/notify but allows multiple named conditions on a single lock and supports timed waits more cleanly.CountDownLatch,CyclicBarrier,Semaphore— purpose-built synchronizers for one-time countdowns, rendezvous points, and rate limiting.
BlockingQueue or Condition. Understanding wait/notify remains essential: it underpins every higher-level tool, it appears in legacy codebases you will need to maintain, and interview questions routinely test it. It is also the only option when you cannot introduce a dependency on java.util.concurrent.
Summary
Guarded blocks built around wait and notifyAll are the foundation of inter-thread coordination in Java. The rules are strict but consistent: always hold the monitor, always loop, always prefer notifyAll, and always propagate InterruptedException. In the next lesson we will examine deadlocks — what they are, how to diagnose them, and how to design your way out of them.