Java Inter-Thread Communication
A thread sometimes needs to wait for a condition instead of repeatedly checking it. With wait(), notify(), and notifyAll(), threads coordinate using the same object monitor and condition. Waking up means the waiter must check again after it gets the lock.
These methods are defined in the Object class.
What is Inter-Thread Communication?
Inter-thread communication is a mechanism that allows one thread to wait for a condition or event and another thread to notify it when that condition may have changed.
For example:
Producer Thread
↓
Produces data
↓
Notifies
↓
Consumer Thread
↓
Processes dataInstead of repeatedly checking whether data is available, the consumer can wait until the producer notifies it.
Why is Inter-Thread Communication Needed?
Suppose one thread produces data and another thread consumes it.
Without proper coordination, the consumer might continuously check:
while (!dataAvailable) {
// keep checking
}This approach is called busy waiting or busy spinning.
It can unnecessarily consume CPU resources.
With inter-thread communication, the consumer can wait:
wait();The producer can then notify the waiting thread:
notify();This allows the waiting thread to pause until there is a reason to continue.
Methods Used for Inter-Thread Communication
Java's traditional monitor-based communication uses three methods:
| Method | Purpose |
|---|---|
| wait() | Makes the current thread wait and releases the object's monitor |
| notify() | Wakes one waiting thread |
| notifyAll() | Wakes all threads waiting on the same object's monitor |
All three methods are defined in the Object class.
wait() Method
The wait() method causes the current thread to wait until another thread notifies it or until it is interrupted.
Example
synchronized (lock) {
lock.wait();
}When wait() is called:
- The current thread must own the object's monitor.
- The thread releases that object's monitor.
- The thread enters the WAITING state.
- It remains waiting until it is notified, interrupted, or otherwise becomes eligible to continue according to the synchronization protocol.
- After being notified, it must reacquire the monitor before continuing.
Why Does wait() Release the Lock?
Suppose a thread calls:
synchronized (lock) {
lock.wait();
}If the thread kept holding the monitor while waiting, another thread would not be able to enter:
synchronized (lock) {
// notify waiting thread
}Therefore, wait() releases the monitor associated with that object while the thread waits.
Conceptually:
Thread-1
↓
owns lock
↓
wait()
↓
releases lock
↓
WAITINGAnother thread can now acquire the same monitor.
notify() Method
The notify() method wakes one thread that is waiting on the same object's monitor.
Example
synchronized (lock) {
lock.notify();
}If multiple threads are waiting, notify() selects one waiting thread to wake.
The exact choice of which waiting thread is notified is not something application code should depend on.
notifyAll() Method
The notifyAll() method wakes all threads waiting on the same object's monitor.
Example
synchronized (lock) {
lock.notifyAll();
}All waiting threads become eligible to compete for the monitor.
However, they do not all execute the synchronized block simultaneously.
Only one thread can own the monitor at a time.
notify() vs notifyAll()
| notify() | notifyAll() |
|---|---|
| Wakes one waiting thread | Wakes all waiting threads |
| Useful when one waiting thread is sufficient | Useful when multiple waiting threads may need to recheck a condition |
| Other waiting threads remain waiting | Other waiting threads also become eligible to compete for the monitor |
Example
lock.notify();wakes one waiting thread. Whereas:
lock.notifyAll();wakes all threads waiting on that monitor.
Important Rule: wait(), notify(), and notifyAll() Require the Monitor
These methods must be called by a thread that owns the monitor of the object.
Correct:
synchronized (lock) {
lock.wait();
}Correct:
synchronized (lock) {
lock.notify();
}Correct:
synchronized (lock) {
lock.notifyAll();
}Incorrect:
lock.wait();without owning lock's monitor. This results in:
IllegalMonitorStateExceptionBasic Producer-Consumer Example
A common example of inter-thread communication is the producer-consumer problem.
The producer produces data. The consumer consumes data.
Producer
↓
Produces data
↓
Shared resource
↓
Consumer
↓
Consumes dataLet's create a simple example.
class SharedData {
private int value;
private boolean available = false;
public synchronized void produce(int value) {
while (available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
this.value = value;
available = true;
System.out.println("Produced: " + value);
notifyAll();
}
public synchronized int consume() {
while (!available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
}
}
int value = this.value;
available = false;
System.out.println("Consumed: " + value);
notifyAll();
return value;
}
}Here:
- produce() waits if data is already available.
- consume() waits if data is not available.
- notifyAll() informs waiting threads that the shared state has changed.
Complete Producer-Consumer Example
class SharedData {
private int value;
private boolean available = false;
public synchronized void produce(int value) {
while (available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
this.value = value;
available = true;
System.out.println("Produced: " + value);
notifyAll();
}
public synchronized int consume() {
while (!available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
}
}
int value = this.value;
available = false;
System.out.println("Consumed: " + value);
notifyAll();
return value;
}
}
public class HelloWorld {
public static void main(String[] args) {
SharedData data = new SharedData();
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
data.produce(i);
}
});
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
data.consume();
}
});
producer.start();
consumer.start();
}
}Possible output:
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5The exact scheduling and output order can vary.
Why Use while Instead of if with wait()?
This is a very important rule.
Prefer:
while (!available) {
wait();
}instead of:
if (!available) {
wait();
}A thread that wakes up must recheck the condition before proceeding.
There are several reasons for this, including:
- Another thread may have changed the shared state before the awakened thread reacquires the monitor.
- notifyAll() can wake multiple threads, but only one may successfully proceed first.
- Java permits spurious wakeups.
Therefore, the condition should normally be checked in a loop.
The general pattern is:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// Continue when condition is satisfied
}Spurious Wakeups
A waiting thread can sometimes return from wait() without receiving the notification that the application expected.
This is known as a spurious wakeup.
Therefore, code should not assume:
wait();means the desired condition is definitely true afterward.
Instead, always recheck the condition:
while (!condition) {
wait();
}This is one of the main reasons while is preferred over if.
Inter-Thread Communication Flow
Consider a consumer waiting for data.
Consumer Thread
↓
Check condition
↓
Data unavailable
↓
wait()
↓
Releases monitor
↓
WAITINGThe producer then produces data:
Producer Thread
↓
Acquires monitor
↓
Produces data
↓
notifyAll()
↓
Releases monitorThe consumer becomes eligible to continue:
Consumer
↓
Reacquires monitor
↓
Checks condition again
↓
Processes dataThe important point is that notification does not directly transfer the monitor to the waiting thread.
The awakened thread must reacquire the monitor before continuing.
wait() with Timeout
Java also provides a timed version of wait():
wait(2000);This means the thread can wait for up to approximately 2 seconds.
Example
synchronized (lock) {
while (!condition) {
lock.wait(2000);
}
}The thread may resume earlier if it is notified, interrupted, or if the waiting period expires.
Unlike plain wait(), timed waiting places the thread in the TIMED_WAITING state.
wait() vs sleep()
wait() and sleep() are frequently confused.
| wait() | sleep() |
|---|---|
| Defined in Object | Defined in Thread |
| Used for thread coordination | Used to pause a thread for a period |
| Releases the monitor of the object on which it waits | Does not release monitors held by the sleeping thread |
| Must be called while owning that object's monitor | Does not require a monitor |
| Can wait indefinitely or for a specified time | Always specifies a duration |
| Can be paired with notify() / notifyAll() | Notified by notify() is unrelated to sleep() |
Example
synchronized (lock) {
lock.wait();
}releases lock's monitor. But:
synchronized (lock) {
Thread.sleep(2000);
}does not release lock's monitor.
notify() Does Not Release the Lock Immediately
Consider:
synchronized (lock) {
lock.notify();
}Calling notify() wakes a waiting thread, but the notifying thread still owns the monitor until it exits the synchronized block or otherwise releases the monitor.
The notified thread must wait to reacquire the monitor.
Conceptually:
Thread-1
↓
owns monitor
↓
notify()
↓
Thread-2 becomes eligible
↓
Thread-1 continues
↓
Thread-1 exits synchronized block
↓
Thread-2 can acquire monitornotify() vs notifyAll() Example
Suppose three threads are waiting:
Thread-1 → WAITING
Thread-2 → WAITING
Thread-3 → WAITINGIf we call:
notify();one waiting thread is selected to be notified.
Conceptually:
Thread-1 → notified
Thread-2 → WAITING
Thread-3 → WAITINGIf we call:
notifyAll();all three become eligible to compete for the monitor:
Thread-1 → eligible
Thread-2 → eligible
Thread-3 → eligibleThey do not all enter the synchronized section at the same time.
Inter-Thread Communication Using a Shared Lock
A common pattern is to create a dedicated lock object:
class SharedData {
private final Object lock = new Object();
public void waitForData() throws InterruptedException {
synchronized (lock) {
while (!isDataAvailable()) {
lock.wait();
}
// Process data
}
}
public void signalDataAvailable() {
synchronized (lock) {
// Update shared state
lock.notifyAll();
}
}
private boolean isDataAvailable() {
return true;
}
}The same lock object is used for protecting the shared state, waiting, and notification.
Using a common monitor is essential for this communication pattern.
Thread States During Inter-Thread Communication
Inter-thread communication commonly involves these thread states.
WAITING
When:
wait();is called without a timeout. The thread enters WAITING.
TIMED_WAITING
When:
wait(2000);is called. The thread enters TIMED_WAITING.
BLOCKED
After a waiting thread is notified, it must reacquire the monitor.
If another thread currently owns the monitor, the notified thread may enter BLOCKED while waiting to acquire it.
Conceptually:
RUNNABLE
↓
wait()
↓
WAITING
↓
notify()
↓
BLOCKED
↓
acquires monitor
↓
RUNNABLEThis is an important part of understanding how wait() and notify() work.
wait() and Interruption
A thread waiting with wait() can be interrupted.
Example
try {
synchronized (lock) {
lock.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}When a waiting thread is interrupted, wait() throws InterruptedException.
A common practice is to restore the thread's interrupt status when the code cannot handle the interruption immediately:
Thread.currentThread().interrupt();Important Rule: wait() Is Not a Thread Method
A common misconception is that wait() belongs to Thread.
It does not. wait() is defined in:
java.lang.ObjectTherefore, it can be called on an object whose monitor is being used:
lock.wait();Similarly:
lock.notify();
lock.notifyAll();are methods of Object.
wait(), notify(), and notifyAll() Belong to Object
The methods are defined in the Object class:
public final void wait()
public final native void wait(long timeoutMillis)
public final void notify()
public final native void notifyAll()The exact low-level implementation details are JVM-specific, but from a Java programming perspective, these methods are used with an object's monitor for thread coordination.
Common Mistakes
Mistake 1: Calling wait() Outside Synchronization
Incorrect:
lock.wait();Correct:
synchronized (lock) {
lock.wait();
}Otherwise, IllegalMonitorStateException can occur.
Mistake 2: Calling notify() on a Different Object
Suppose one thread waits using:
synchronized (lock1) {
lock1.wait();
}Another thread must notify using the same monitor:
synchronized (lock1) {
lock1.notify();
}Using another object:
synchronized (lock2) {
lock2.notify();
}does not notify the thread waiting on lock1.
Mistake 3: Using if Instead of while
Avoid:
if (!condition) {
wait();
}Prefer:
while (!condition) {
wait();
}The condition must be checked again after the thread wakes up.
Mistake 4: Assuming notify() Immediately Runs the Waiting Thread
notify() only makes one waiting thread eligible to continue.
The notifying thread still owns the monitor until it releases it.
Mistake 5: Assuming notifyAll() Runs All Threads Together
notifyAll() wakes all waiting threads, but only one thread can own a particular monitor at a time.
The awakened threads compete to reacquire the monitor.
Inter-Thread Communication vs Synchronization
These concepts are related but not identical.
Synchronization
Synchronization controls access to shared resources.
synchronized (lock) {
// protected code
}Inter-Thread Communication
Inter-thread communication allows threads to coordinate based on conditions.
synchronized (lock) {
while (!condition) {
lock.wait();
}
lock.notifyAll();
}A simple way to remember:
Synchronization
↓
Controls access
Inter-thread communication
↓
Coordinates threadsInter-thread communication commonly relies on synchronization and monitors when using wait(), notify(), and notifyAll().
Producer-Consumer Concept
The producer-consumer problem is one of the most common examples of inter-thread communication.
Producer
|
| produces
v
+---------------+
| Shared Buffer |
+---------------+
|
| consumes
v
ConsumerThe producer waits when the buffer is full.
The consumer waits when the buffer is empty.
When the state changes, the corresponding thread can notify waiting threads.
For real applications, Java's concurrency utilities such as BlockingQueue often provide a safer and simpler implementation of producer-consumer coordination.
Using BlockingQueue
Instead of manually implementing wait() and notify(), Java provides higher-level concurrency utilities.
For example:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
public class HelloWorld {
public static void main(String[] args) {
BlockingQueue<Integer> queue =
new ArrayBlockingQueue<>(10);
Thread producer = new Thread(() -> {
try {
queue.put(100);
System.out.println("Produced: 100");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
int value = queue.take();
System.out.println("Consumed: " + value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}Here:
put()
↓
adds data
take()
↓
removes dataBlockingQueue handles the waiting and notification mechanics internally.
This is often preferable to implementing a producer-consumer queue manually.
When Should You Use wait(), notify(), and notifyAll()?
These methods are useful for understanding Java's monitor-based thread communication and can be appropriate in carefully designed low-level synchronization code.
However, for many production applications, higher-level concurrency utilities are preferable.
Examples include:
- BlockingQueue
- ExecutorService
- Future
- CompletableFuture
- CountDownLatch
- Semaphore
- CyclicBarrier
- Phaser
- Lock and Condition
These abstractions can make concurrent programs easier to design and maintain.
Advantages of Inter-Thread Communication
- Allows threads to coordinate their activities.
- Avoids unnecessary busy waiting.
- Helps implement producer-consumer patterns.
- Allows a thread to wait until a required condition becomes true.
- Provides a mechanism for notifying waiting threads.
- Can improve CPU efficiency compared with continuous polling.
Limitations and Risks
- Incorrect use can cause deadlocks.
- Incorrect condition handling can cause missed coordination or threads waiting indefinitely.
- Using if instead of while around wait() can lead to incorrect behavior.
- notify() may wake a thread that cannot currently make progress.
- Complex communication logic can become difficult to maintain.
- Higher-level concurrency utilities are often easier to use safely.
Key Takeaways
- Inter-thread communication allows threads to coordinate with each other.
- Java provides wait(), notify(), and notifyAll() for traditional monitor-based communication.
- These methods are defined in the Object class.
- wait() releases the relevant object's monitor while the thread waits.
- notify() wakes one waiting thread.
- notifyAll() wakes all threads waiting on the same monitor.
- The notified thread must reacquire the monitor before continuing.
- wait(), notify(), and notifyAll() must be called while owning the relevant object's monitor.
- Always recheck a waiting condition using a while loop.
- sleep() does not release a monitor.
- wait() can result in WAITING or TIMED_WAITING.
- A notified thread may temporarily enter BLOCKED while waiting to reacquire the monitor.
- BlockingQueue is often a better choice for producer-consumer applications than implementing wait() and notify() manually.
Simple Concept to Remember
Thread-1
|
wait()
|
v
WAITING
|
|
notify/notifyAll
|v Reacquire monitor | v Continue
The key idea is: Inter-thread communication allows threads to wait for conditions and notify each other when the shared state changes.
See you in next page....🙂