Inter-Thread Communication
Inter-thread communication allows synchronized threads to communicate with each other. It is a mechanism in which a thread pauses running in its critical section and allows another thread to enter (or lock) the same critical section to be executed.
This is primarily achieved using three methods that belong to the Object class (not the Thread class):
wait()notify()notifyAll()
Why use Inter-Thread Communication?
Imagine a classic Producer-Consumer problem. A Producer thread produces data and puts it into a buffer, and a Consumer thread consumes that data.
- If the buffer is full, the Producer must wait until the Consumer consumes some data.
- If the buffer is empty, the Consumer must wait until the Producer produces some data.
Polling (continuously checking the buffer state in a while(true) loop) wastes CPU cycles. Inter-thread communication solves this gracefully by pausing the threads and waking them up only when the condition changes.
The Methods
1. wait()
Causes the current thread to release the lock and wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
- Must be called from a
synchronizedblock/method. - The thread enters the
WAITINGstate.
2. notify()
Wakes up a single thread that is waiting on this object's monitor (lock).
- If multiple threads are waiting, only one of them is awakened (the choice is arbitrary and depends on the JVM implementation).
- Must be called from a
synchronizedblock/method.
3. notifyAll()
Wakes up all threads that are waiting on this object's monitor.
- Must be called from a
synchronizedblock/method.
Example: Bank Withdrawal
Let's look at an example where a user tries to withdraw money, but their balance is too low. The withdrawal thread will wait() until a deposit thread adds enough money to the account and calls notify().
class Customer {
int amount = 10000;
synchronized void withdraw(int withdrawalAmount) {
System.out.println("Going to withdraw...");
if (this.amount < withdrawalAmount) {
System.out.println("Less balance; waiting for deposit...");
try {
// Releases the lock and waits for a notify() signal
wait();
} catch (Exception e) {
System.out.println(e);
}
}
// This executes after notify() is called and the lock is reacquired
this.amount -= withdrawalAmount;
System.out.println(
"Withdrawal completed! Remaining Balance: " + this.amount
);
}
synchronized void deposit(int depositAmount) {
System.out.println("Going to deposit...");
this.amount += depositAmount;
System.out.println("Deposit completed!");
// Wakes up a single thread waiting on this object's lock
notify();
}
}
public class LabInterThread1 {
public static void main(String args[]) {
Customer c = new Customer();
// Thread 1: Tries to withdraw 15000
new Thread() {
public void run() {
c.withdraw(15000);
}
}
.start();
// Thread 2: Deposits 10000
new Thread() {
public void run() {
c.deposit(10000);
}
}
.start();
}
}
Execution Flow:
- Thread 1 calls
withdraw(15000). It acquires the lock. - The condition
10000 < 15000is true. It prints "Less balance; waiting for deposit..." and callswait(). wait()releases the lock on theCustomerobject and Thread 1 goes to sleep.- Thread 2 calls
deposit(10000). Since the lock was released, it acquires the lock. - It adds
10000to the balance (making it20000), prints "Deposit completed!", and callsnotify(). notify()wakes up Thread 1. Thread 2 releases the lock as the method finishes.- Thread 1 reacquires the lock, finishes the withdrawal (balance becomes
5000), and prints "Withdrawal completed!".
[!IMPORTANT] The
wait(),notify(), andnotifyAll()methods must always be called inside asynchronizedcontext. If you call them outside a synchronized context, the JVM will throw anIllegalMonitorStateExceptionat runtime.