Producer Thread | | produces data v Shared Data | | waits until data exists vConsumer ThreadInter-Thread Communication in Java: wait(), notify(), and notifyAll()
Direct Answer & Core Concept
When multiple threads work together, synchronization alone is sometimes not enough. Threads may also need a way to wait for a condition, signal another thread that something has changed, and coordinate their work.
Java provides three classic methods for this purpose:
wait()notify()notifyAll()
These methods are defined by the Object class and work with an object's monitor lock.
They are especially useful for understanding classic coordination patterns such as producer-consumer, where one thread produces data and another thread waits until that data becomes available.
Detailed Explanation & Mechanics
What Is Inter-Thread Communication?
Inter-thread communication is the process through which threads coordinate their execution.
Imagine a producer and consumer:
The consumer should not continuously check the data in a tight loop.
Instead, it can wait until the producer signals that the required condition may have changed.
This is one of the purposes of wait() and notification methods.
Why Do Threads Need to Communicate?
Suppose a consumer wants to process an item from a shared buffer.
If the buffer is empty, the consumer cannot continue.
One inefficient approach would be:
while (buffer.isEmpty()) {
// keep checking
}This is called busy waiting or spinning.
The thread repeatedly checks the condition and can waste CPU time.
A better approach is:
Buffer empty↓Consumer waits↓Producer adds item↓Producer signals↓Consumer wakes↓Consumer checks condition↓Consumer processes itemJava's wait() and notification methods support this style of coordination.
The wait() Method
The wait() method tells the current thread to wait until it is notified or otherwise interrupted/awakened.
A simplified example is:
synchronized (lock) {
lock.wait();
}An important point is that the thread must own the monitor associated with the object on which wait() is called.
For example:
synchronized (lock) {
lock.wait();
}is valid.
But this is not:
lock.wait();when the current thread does not own lock's monitor.
Calling wait() without owning the object's monitor results in IllegalMonitorStateException.
What Happens When wait() Is Called?
When a thread calls:
lock.wait();while holding lock's monitor, several things happen:
The thread enters the object's wait set.
It releases that object's monitor.
It stops executing until it is awakened.
After being awakened, it must reacquire the same monitor before
wait()returns.
The release of the monitor is particularly important.
Thread A | | owns lock | wait() | +---- releases lock | v waitingAnother thread can now acquire the lock and perform the required work.
wait() Releases the Monitor
This is one of the most important differences between wait() and sleep().
When a thread calls:
lock.wait();it releases the monitor associated with lock.
By contrast:
Thread.sleep(1000);does not release monitors that the thread currently holds.
Comparison
| Operation | Releases monitor? |
|---|---|
wait() | Yes |
sleep() | No |
join() | The waiting thread may block, but it does not generally release unrelated monitors it holds |
For wait(), the monitor release is essential because it allows another thread to acquire the lock and change the condition being waited for.
The notify() Method
The notify() method wakes one thread waiting on the same object's monitor.
Example:
synchronized (lock) {
lock.notify();
}The calling thread must own the monitor.
Waiting threads:Thread AThread BThread C | notify() | vOne waiting thread is selectedThe exact waiting thread selected is not something your program should depend on.
Also, notify() does not immediately transfer the lock to the awakened thread.
The notifying thread still owns the monitor until it exits the synchronized region or otherwise releases the monitor.
The notifyAll() Method
notifyAll() wakes all threads waiting on the same object's monitor.
synchronized (lock) {
lock.notifyAll();
}Conceptually:
Waiting threads:Thread AThread BThread CThread D | notifyAll() | vAll become eligible to continueHowever, they do not all execute inside the synchronized block simultaneously.
Only one thread can own the monitor at a time.
After waking, each thread must reacquire the lock before continuing.
notify() vs notifyAll()
The basic difference is:
notify()↓Wakes one waiting threadnotifyAll()↓Wakes all waiting threadsA common mistake is assuming that notify() means "wake the thread I want."
It does not provide that kind of selection mechanism.
For many condition-based designs, notifyAll() is safer because all waiting threads get an opportunity to re-check whether their condition is now satisfied.
Why wait() Should Usually Be Inside a while Loop
Consider:
synchronized (lock) {
if (!condition) {
lock.wait();
} // continue
}This pattern is generally unsafe.
The preferred structure is:
synchronized (lock) {
while (!condition) {
lock.wait();
} // continue
}Why?
Because waking from wait() does not mean the condition is definitely true.
A thread can wake because:
Another thread called
notify().Another thread called
notifyAll().The thread was interrupted.
A spurious wakeup occurred.
The condition must therefore be checked again after waking.
The while loop ensures the thread only proceeds when the required condition is actually satisfied.
Basic Example
Here is a simple communication example:
class Message {
private String message;
private boolean available = false;
public synchronized void produce(String message) throws InterruptedException {
while (available) {
wait();
} this.message = message;
available = true;
notifyAll();
} public synchronized String consume() throws InterruptedException {
while (!available) {
wait();
} String result = message;
available = false;
notifyAll();
return result;
}
}The producer waits while the previous message is still available.
The consumer waits while no message is available.
After changing the state, each method calls notifyAll() to allow waiting threads to re-check their conditions.
Producer-Consumer Pattern
The producer-consumer pattern is one of the most common examples of inter-thread communication.
Producer | | produce v +-------------+ | Buffer | +-------------+ | | consume v ConsumerThe producer adds data to the buffer.
The consumer removes data from the buffer.
If the buffer is empty, the consumer waits.
If the buffer is full, the producer waits.
Producer-Consumer Example
Let's build a simple one-item buffer.
class SharedBuffer {
private int value;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait();
} this.value = value;
available = true;
System.out.println( "Produced: " + value);
notifyAll();
} public synchronized int consume() throws InterruptedException {
while (!available) {
wait();
} int result = value;
available = false;
System.out.println( "Consumed: " + result);
notifyAll();
return result;
}
}Now create producer and consumer threads:
public class Main {
public static void main(String[] args) {
SharedBuffer buffer = new SharedBuffer();
Thread producer = new Thread(() -> {
try {
for (int i = 1;
i <= 5;
i++) {
buffer.produce(i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 1;
i <= 5;
i++) {
buffer.consume();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}A possible output is:
Produced: 1Consumed: 1Produced: 2Consumed: 2Produced: 3Consumed: 3Produced: 4Consumed: 4Produced: 5Consumed: 5The exact scheduling order depends on thread execution.
Understanding the Producer
The producer contains:
while (available) {
wait();
}If the buffer already contains an item, the producer waits.
Once it is allowed to continue, it stores the new value:
this.value = value;
available = true;Then:
notifyAll();signals waiting threads that the state has changed.
Understanding the Consumer
The consumer contains:
while (!available) {
wait();
}If there is no item, the consumer waits.
When an item becomes available:
int result = value;
available = false;The consumer retrieves it and changes the state.
Then:
notifyAll();allows waiting threads to check their conditions again.
The Monitor Must Match
A crucial rule is that wait(), notify(), and notifyAll() operate on a specific object monitor.
For example:
synchronized (lock) {
lock.wait();
}The same object is involved in both operations.
synchronized (lock) {
lock.notifyAll();
}A thread waiting on lockA will not be awakened by:
lockB.notifyAll();because lockA and lockB are different monitor objects.
IllegalMonitorStateException
This code is incorrect:
Object lock = new Object();
lock.wait();The current thread does not own the monitor.
You need:
synchronized (lock) {
lock.wait();
}Likewise:
synchronized (lock) {
lock.notify();
}or:
synchronized (lock) {
lock.notifyAll();
}The calling thread must own the object's monitor when invoking these methods.
wait() With a Timeout
Java also provides a timed version:
wait(1000);This means the thread waits for up to the specified time, subject to notification, interruption, and other scheduling conditions.
Example:
synchronized (lock) {
while (!ready) {
lock.wait(1000);
}
}The timeout does not remove the need to re-check the condition.
notify() Does Not Release the Lock Immediately
Consider:
synchronized (lock) {
lock.notify();
System.out.println("Still holding lock");
}Calling notify() wakes a waiting thread, but the current thread still owns the monitor while it remains inside the synchronized region.
The awakened thread must wait until it can reacquire the monitor.
So this:
notify()↓Waiting thread becomes eligible↓Current thread still owns lock↓Current thread exits synchronized block↓Waiting thread can acquire locknotifyAll() Does Not Mean All Threads Run Together
Suppose five threads are waiting:
Thread AThread BThread CThread DThread ECalling:
notifyAll();makes all of them eligible to compete for the monitor.
It does not mean all five enter the synchronized block simultaneously.
Only one thread can own the monitor at a time.
Each thread must reacquire the lock and re-check its condition.
Why notifyAll() Is Often Safer
Imagine several types of threads are waiting for different conditions on the same monitor.
Using:
notify();might wake a thread whose condition is still false.
That thread checks its condition and waits again.
Another thread whose condition is now true may remain asleep.
Using:
notifyAll();allows all waiting threads to wake and check their conditions.
For this reason, notifyAll() is often a safer choice when multiple condition types share the same monitor.
Spurious Wakeups
A waiting thread can occasionally wake without the specific notification you expected.
Therefore, avoid:
if (!ready) {
wait();
}Prefer:
while (!ready) {
wait();
}The loop makes the code robust because the condition is checked again before the thread proceeds.
wait() vs sleep()
These methods are frequently confused.
| Feature | wait() | sleep() |
|---|---|---|
| Declared by | Object | Thread |
| Requires owning monitor | Yes | No |
| Releases object's monitor | Yes | No |
| Used for condition coordination | Yes | No |
| Can be timed | Yes | Yes |
| Can be interrupted | Yes | Yes |
Example:
synchronized (lock) {
lock.wait();
}versus:
Thread.sleep(1000);wait() is about coordination around a monitor and condition.
sleep() is about temporarily pausing the current thread.
wait() vs notify() vs notifyAll()
A simple way to remember them:
wait()↓"I cannot continue yet."notify()↓"One waiting thread may try again."notifyAll()↓"All waiting threads may try again."The waiting threads must still re-check their conditions.
Handling InterruptedException
wait() declares InterruptedException.
A common pattern when you are not able to handle the interruption locally is:
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Restoring the interrupt status is often appropriate when the current method cannot fully handle the interruption.
Similarly:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Thread interruption is cooperative; it is not a forced termination mechanism.
Example With a Ready Flag
Here is a simple coordination example:
class Task {
private boolean ready = false;
public synchronized void waitUntilReady() throws InterruptedException {
while (!ready) {
wait();
} System.out.println("Task is ready");
} public synchronized void makeReady() {
ready = true;
notifyAll();
}
}Usage:
public class Main {
public static void main(String[] args) {
Task task = new Task();
Thread worker = new Thread(() -> {
try {
task.waitUntilReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
worker.start();
Thread controller = new Thread(() -> {
task.makeReady();
});
controller.start();
}
}The worker waits until the ready condition becomes true.
A Better Modern Alternative: BlockingQueue
Although wait() and notifyAll() are important concepts to learn, modern Java applications often use higher-level concurrency utilities instead of manually implementing producer-consumer communication.
For example:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);Producer:
queue.put(100);Consumer:
int value = queue.take();The queue handles much of the waiting and notification logic internally.
This is often easier to reason about than manually managing wait() and notifyAll().
Why Learn wait() and notifyAll()?
Even when you use higher-level concurrency APIs, understanding these methods is valuable because they explain fundamental Java concurrency concepts.
You learn:
monitor ownership
waiting for conditions
mutual exclusion
thread notification
condition re-checking
producer-consumer coordination
interrupt handling
monitor-based synchronization
These concepts also help when reading older Java code that directly uses intrinsic monitors.
Common Mistakes
Mistake 1: Calling wait() Outside Synchronization
Incorrect:
lock.wait();Correct:
synchronized (lock) {
lock.wait();
}Mistake 2: Using if Instead of while
Avoid:
if (!ready) {
wait();
}Prefer:
while (!ready) {
wait();
}The condition must be checked again after waking.
Mistake 3: Confusing sleep() With wait()
sleep() does not release the monitor.
wait() releases the monitor associated with the object on which it is called.
Mistake 4: Assuming notify() Chooses a Specific Thread
notify() does not let you specify which waiting thread should wake.
If your design requires all waiting threads to re-check their conditions, notifyAll() may be more appropriate.
Mistake 5: Forgetting to Change the Condition
Calling:
notifyAll();without changing the relevant shared state often does not solve the coordination problem.
Typically, the notifying thread changes the state first:
ready = true;
notifyAll();Mistake 6: Using Different Locks
If one thread waits on:
lockA.wait();another thread cannot wake it using:
lockB.notifyAll();The monitor must be the same object.
Best Practices
1. Always wait in a loop
Use:
while (!condition) {
wait();
}2. Protect the condition with the same monitor
The condition check, state update, waiting, and notification should be designed around the appropriate shared lock.
3. Change state before notifying
A typical pattern is:
synchronized (lock) {
condition = true;
lock.notifyAll();
}4. Keep synchronization focused
Avoid holding locks while performing unnecessary slow operations.
5. Handle interruption correctly
Do not silently swallow InterruptedException unless there is a deliberate reason.
6. Prefer higher-level concurrency utilities when suitable
For producer-consumer systems, BlockingQueue is often easier and safer than manually implementing the complete coordination mechanism.
Frequently Asked Questions
What is inter-thread communication?
It is a mechanism that allows threads to coordinate their execution based on shared state or conditions.
What are wait(), notify(), and notifyAll()?
They are methods of Object used with an object's monitor to coordinate threads waiting for conditions.
Does wait() release the lock?
Yes. When a thread calls wait() on an object's monitor, it releases that monitor while waiting.
Does sleep() release the lock?
No. Thread.sleep() does not release monitors held by the sleeping thread.
Can wait() be called without synchronized?
Not when using the object's monitor. The current thread must own that object's monitor or IllegalMonitorStateException is thrown.
What does notify() do?
It wakes one thread waiting on the same object's monitor, making that thread eligible to continue after it reacquires the monitor.
What does notifyAll() do?
It wakes all threads waiting on the same object's monitor, making them eligible to compete for the monitor and re-check their conditions.
Why should wait() be inside a while loop?
Because waking does not guarantee that the desired condition is true. The condition must be checked again.
Can notifyAll() run all waiting threads simultaneously?
No. Only one thread can own a particular intrinsic monitor at a time.
Is wait() a method of Thread?
No. wait() is defined by Object, which is why every Java object can participate in this monitor-based coordination mechanism.
Is wait() still commonly used in modern Java?
It remains important for understanding Java concurrency and existing code, but higher-level tools such as BlockingQueue, executors, and other concurrency utilities are often preferable for new application designs.
Suggested Graphic
Create a clean producer-consumer communication diagram:
PRODUCER THREAD | | produce item v +------------------+ | Shared Buffer | +------------------+ | | item available v CONSUMER THREADIf buffer is empty: Consumer | wait() | v releases lock | | Producer adds item | notifyAll() | v Consumer wakes | Re-check condition | consume()Highlight the key concept visually:
wait() → release monitor → wait for condition → notifyAll() → reacquire monitor → re-check condition.
Runnable Code Example: Inter-Thread Communication in Java
Java 17+class MessageQueue {
private String message;
private boolean empty = true;
public synchronized String take() throws InterruptedException {
while (empty) {
wait();
// Wait inside while loop to guard against spurious wakeups
} empty = true;
notifyAll();
return message;
} public synchronized void put(String msg) throws InterruptedException {
while (!empty) {
wait();
} this.message = msg;
this.empty = false;
notifyAll();
}
}public class InterThreadDemo {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue();
new Thread(() -> {
try {
queue.put("Order #101 Created");
System.out.println("Produced message");
} catch (InterruptedException e) {
}
}).start();
new Thread(() -> {
try {
String msg = queue.take();
System.out.println("Consumed: " + msg);
} catch (InterruptedException e) {
}
}).start();
}
}