By WebNest Studio Engineering Team
Java - Core Tutorial
Inter-thread Communication: wait, notify, notifyAll
Synchronization alone prevents threads from stepping on each other, but it does not let threads coordinate based on changing conditions — for example, a consumer thread that must pause until a producer thread has data ready. Java's Object class provides three methods for exactly this purpose: wait(), notify(), and notifyAll(), collectively known as inter-thread communication.
Why it matters
Sometimes a thread must not just exclude others but wait for them: a consumer waits for a producer, a worker waits for a signal. wait() and notifyAll() are the low-level mechanism for that. They have two rules that are easy to break — call them only while holding the lock, and always wait in a loop — and breaking either produces bugs that appear only occasionally.
The Rules: Must Be Called From a Synchronized Context
wait(), notify(), and notifyAll() can only be called by a thread that currently holds the monitor of the object they are invoked on — that is, from inside a block or method synchronized on that same object. Calling any of them outside a synchronized context throws IllegalMonitorStateException. Calling wait() releases the monitor and puts the thread into the WAITING state until another thread calls notify() or notifyAll() on that same object; the waiting thread then re-acquires the monitor before continuing.
wait()— releases the lock and waits until notified (or interrupted, or a timeout elapses for the overloaded form).notify()— wakes up a single arbitrary thread waiting on this object's monitor.notifyAll()— wakes up all threads waiting on this object's monitor; each one competes to re-acquire the lock.
Why Use a while Loop, Not an if, Around wait()
A woken thread should always re-check its condition in a loop (while (conditionNotMet) { obj.wait(); }) rather than trusting a single check, because of "spurious wakeups" (the JVM is permitted to wake a waiting thread without an actual notify) and because with notifyAll(), multiple threads wake up but only some of them may find the condition still true for them by the time they re-acquire the lock.
The Producer-Consumer Pattern
The canonical use case is a shared buffer where a producer thread adds items and a consumer thread removes them. The producer waits when the buffer is full; the consumer waits when the buffer is empty; each notifies the other after changing the buffer's state. This pattern is the conceptual foundation behind Java's higher-level BlockingQueue implementations, which handle this coordination internally.
Example
A minimal single-slot producer-consumer using wait/notify
public class ProducerConsumerDemo {
private Integer item = null;
public synchronized void produce(int value) throws InterruptedException {
while (item != null) {
wait(); // slot occupied, wait for consumer to take it
}
item = value;
System.out.println("Produced: " + value);
notifyAll(); // wake the consumer
}
public synchronized void consume() throws InterruptedException {
while (item == null) {
wait(); // nothing to consume yet
}
System.out.println("Consumed: " + item);
item = null;
notifyAll(); // wake the producer
}
public static void main(String[] args) throws InterruptedException {
ProducerConsumerDemo buffer = new ProducerConsumerDemo();
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) buffer.produce(i);
} catch (InterruptedException ignored) {
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) buffer.consume();
} catch (InterruptedException ignored) {
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
(The exact interleaving of "Produced"/"Consumed" lines is coordinated by wait/notify so each value
is consumed once, but timing details can vary slightly between runs.)
Common Mistakes
- Calling wait(), notify(), or notifyAll() outside a synchronized block on that object — this throws IllegalMonitorStateException immediately.
- Using "if (condition) wait();" instead of a while loop, which breaks under spurious wakeups or when notifyAll() wakes threads whose condition is not actually satisfied.
- Calling notify() when multiple different waiting conditions exist on the same object — notify() wakes an arbitrary thread, which might not be the one that should proceed; notifyAll() is safer in that case.
- Forgetting that wait() releases the lock while waiting, and assuming the thread still holds it during that time.
Exercise
Complete the Gate class. pass() must wait until the gate has been opened, and open() must open it and wake any waiting thread.
Expected output: opening then passed
class Gate {
private boolean open = false;
synchronized void pass() throws InterruptedException {
// TODO: wait while the gate is not open
System.out.println("passed");
}
synchronized void open() {
// TODO: open the gate and wake up waiting threads
}
}
public class GateDemo {
public static void main(String[] args) throws InterruptedException {
Gate gate = new Gate();
Thread visitor = new Thread(() -> {
try {
gate.pass();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
visitor.start();
System.out.println("opening");
gate.open();
visitor.join();
}
}
Show hints
- Use
while (!open) { wait(); }, notif, so the condition is checked again after waking. - In
open(), change the flag first and then callnotifyAll().
Show solution
class Gate {
private boolean open = false;
synchronized void pass() throws InterruptedException {
while (!open) {
wait();
}
System.out.println("passed");
}
synchronized void open() {
open = true;
notifyAll();
}
}
public class GateDemo {
public static void main(String[] args) throws InterruptedException {
Gate gate = new Gate();
Thread visitor = new Thread(() -> {
try {
gate.pass();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
visitor.start();
System.out.println("opening");
gate.open();
visitor.join();
}
}
Quick quiz
1. What happens if wait() is called without holding the object's lock?
- A. It throws IllegalMonitorStateException
- B. It waits normally
- C. It does not compile
- D. It returns immediately
Show answer
A. wait, notify and notifyAll must be called from inside a synchronized block or method on that same object.
2. Does wait() release the lock while the thread is waiting?
- A. No, it keeps the lock
- B. Yes, and reacquires it before returning
- C. Only with a timeout
- D. Only for static methods
Show answer
B. Releasing the lock is what allows another thread to enter, change the condition and call notify.
3. Why should wait() be called inside a while loop rather than an if?
- A. It runs faster
- B. if is not allowed with wait
- C. A thread can wake without the condition being true, so it must check again
- D. To avoid compiling errors
Show answer
C. Spurious wake-ups can happen, and another thread may have changed the condition again before this one reacquires the lock.
Interview questions
What is the difference between notify() and notifyAll()?
Show answer
notify() wakes one thread waiting on the object, chosen arbitrarily. notifyAll() wakes all of them; they then compete for the lock and each rechecks its condition. notifyAll() is the safer default, because with notify() the one thread that wakes may be waiting for a different condition, leaving the right thread asleep.
Why are wait() and notify() defined in Object rather than Thread?
Show answer
Because they operate on a lock, and in Java every object has its own lock (monitor). A thread waits on a particular object's monitor and is notified through that same object, so the methods belong to the object that owns the lock, not to the thread.
Key Points to Remember
- wait(), notify(), and notifyAll() must be called from within a block synchronized on the same object, or they throw IllegalMonitorStateException.
- wait() releases the monitor and suspends the thread; notify()/notifyAll() wake waiting threads, which must then re-acquire the lock.
- Always re-check the waiting condition in a while loop to guard against spurious wakeups and notifyAll() side effects.
- The producer-consumer pattern is the classic use case for this coordination mechanism.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering