Skip to main content

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 synchronized block/method.
  • The thread enters the WAITING state.

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 synchronized block/method.

3. notifyAll()

Wakes up all threads that are waiting on this object's monitor.

  • Must be called from a synchronized block/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:

  1. Thread 1 calls withdraw(15000). It acquires the lock.
  2. The condition 10000 < 15000 is true. It prints "Less balance; waiting for deposit..." and calls wait().
  3. wait() releases the lock on the Customer object and Thread 1 goes to sleep.
  4. Thread 2 calls deposit(10000). Since the lock was released, it acquires the lock.
  5. It adds 10000 to the balance (making it 20000), prints "Deposit completed!", and calls notify().
  6. notify() wakes up Thread 1. Thread 2 releases the lock as the method finishes.
  7. Thread 1 reacquires the lock, finishes the withdrawal (balance becomes 5000), and prints "Withdrawal completed!".

[!IMPORTANT] The wait(), notify(), and notifyAll() methods must always be called inside a synchronized context. If you call them outside a synchronized context, the JVM will throw an IllegalMonitorStateException at runtime.