Java Inter-Thread Communication

Learn how threads communicate with each other using wait(), notify(), and notifyAll() methods in Java.

What is Inter-Thread Communication?

Inter-thread communication is a mechanism where threads communicate with each other by sending signals. It allows synchronized threads to coordinate their activities and avoid polling (repeatedly checking a condition).

Think of it like a relay race - one runner passes the baton to another and waits for their turn again. Similarly, threads can signal each other when to proceed or wait.

  • Threads can wait for signals from other threads
  • Threads can notify other waiting threads to resume execution
  • This prevents CPU wastage from busy-waiting

Important Note: Inter-thread communication methods (wait, notify, notifyAll) must be called from a synchronized context, otherwise you'll get IllegalMonitorStateException.

Inter-Thread Communication Methods

Java provides three methods in the Object class for inter-thread communication:

1. wait() Method

Causes the current thread to wait until another thread calls notify() or notifyAll() on the same object. The thread releases the lock while waiting.

Syntax: wait() or wait(long timeout)

2. notify() Method

Wakes up a single thread that is waiting on the object's monitor. If multiple threads are waiting, one of them is chosen arbitrarily.

Syntax: notify()

3. notifyAll() Method

Wakes up all threads that are waiting on the object's monitor. All awakened threads compete for the lock, and one will proceed while others wait again.

Syntax: notifyAll()

Why Use Inter-Thread Communication?

Consider a scenario where one thread produces data and another consumes it. Without communication:

  • Consumer Problem: Consumer keeps checking if data is available (busy-waiting wastes CPU)
  • Producer Problem: Producer might overwrite data before consumer reads it
  • Synchronization Alone: Not enough - we need threads to signal each other

Solution: Using wait() and notify(), the consumer waits when no data is available, and the producer notifies when data is ready. This saves CPU and ensures proper coordination.

Simple Wait and Notify Example

Let's see a basic example of how two threads communicate:

Example: Basic Inter-Thread Communication

class SharedResource {
    private boolean dataReady = false;
    
    public synchronized void waitForData() {
        System.out.println(Thread.currentThread().getName() + " is waiting...");
        try {
            while (!dataReady) {
                wait();  // Release lock and wait
            }
            System.out.println(Thread.currentThread().getName() + " resumed!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public synchronized void produceData() {
        System.out.println(Thread.currentThread().getName() + " producing data...");
        try {
            Thread.sleep(2000);  // Simulate work
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        dataReady = true;
        System.out.println(Thread.currentThread().getName() + " data ready!");
        notify();  // Wake up waiting thread
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        
        Thread consumer = new Thread(() -> resource.waitForData(), "Consumer");
        Thread producer = new Thread(() -> resource.produceData(), "Producer");
        
        consumer.start();
        producer.start();
    }
}
Output:
Consumer is waiting...
Producer producing data...
Producer data ready!
Consumer resumed!

Explanation:

  • Consumer calls wait(): Releases the lock and waits for notification
  • Producer does work: Simulates producing data with sleep
  • Producer calls notify(): Wakes up the consumer thread
  • Consumer resumes: Reacquires the lock and continues execution

Producer-Consumer Problem

The classic producer-consumer problem demonstrates inter-thread communication perfectly. One thread produces items, and another consumes them:

Example: Producer-Consumer with Single Item Buffer

class SharedBuffer {
    private int item;
    private boolean available = false;
    
    public synchronized void produce(int value) {
        while (available) {
            try {
                System.out.println("Producer waiting... buffer full");
                wait();  // Wait if buffer is full
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        item = value;
        available = true;
        System.out.println("Produced: " + value);
        notify();  // Notify consumer
    }
    
    public synchronized int consume() {
        while (!available) {
            try {
                System.out.println("Consumer waiting... buffer empty");
                wait();  // Wait if buffer is empty
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        available = false;
        System.out.println("Consumed: " + item);
        notify();  // Notify producer
        return item;
    }
}

class Producer extends Thread {
    SharedBuffer buffer;
    
    Producer(SharedBuffer buffer) {
        this.buffer = buffer;
    }
    
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.produce(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Consumer extends Thread {
    SharedBuffer buffer;
    
    Consumer(SharedBuffer buffer) {
        this.buffer = buffer;
    }
    
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.consume();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedBuffer buffer = new SharedBuffer();
        
        Producer producer = new Producer(buffer);
        Consumer consumer = new Consumer(buffer);
        
        producer.start();
        consumer.start();
    }
}
Output:
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5

Explanation:

  • Producer waits: If buffer is full (available = true), producer calls wait()
  • Consumer waits: If buffer is empty (available = false), consumer calls wait()
  • Mutual notification: After producing/consuming, each thread notifies the other
  • Perfect coordination: Items are produced and consumed in order without data loss

notifyAll() Example

When multiple threads are waiting, use notifyAll() to wake up all of them:

Example: Using notifyAll() for Multiple Threads

class BroadcastMessage {
    private boolean messageAvailable = false;
    
    public synchronized void waitForMessage(String threadName) {
        try {
            while (!messageAvailable) {
                System.out.println(threadName + " is waiting for message...");
                wait();
            }
            System.out.println(threadName + " received the message!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public synchronized void sendMessage() {
        System.out.println("\n--- Broadcasting message to all threads ---\n");
        messageAvailable = true;
        notifyAll();  // Wake up all waiting threads
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        BroadcastMessage broadcast = new BroadcastMessage();
        
        // Create multiple waiting threads
        Thread t1 = new Thread(() -> broadcast.waitForMessage("Thread-1"));
        Thread t2 = new Thread(() -> broadcast.waitForMessage("Thread-2"));
        Thread t3 = new Thread(() -> broadcast.waitForMessage("Thread-3"));
        
        t1.start();
        t2.start();
        t3.start();
        
        Thread.sleep(2000);  // Let all threads start waiting
        
        // Broadcast message to all
        broadcast.sendMessage();
    }
}
Output:
Thread-1 is waiting for message...
Thread-2 is waiting for message...
Thread-3 is waiting for message...

--- Broadcasting message to all threads ---

Thread-1 received the message!
Thread-2 received the message!
Thread-3 received the message!

Explanation:

  • Multiple waiters: Three threads all call wait() and start waiting
  • notifyAll(): Wakes up all three threads simultaneously
  • notify() vs notifyAll(): notify() would wake only one thread, leaving others waiting indefinitely

Important Points to Remember

  • Always use in synchronized context: wait(), notify(), and notifyAll() must be called inside synchronized blocks/methods
  • Use while loop with wait(): Always check the condition in a while loop, not if statement (prevents spurious wakeups)
  • Handle InterruptedException: wait() throws InterruptedException which must be handled
  • notify() vs notifyAll(): Use notify() for single waiter, notifyAll() for multiple waiters
  • Lock release: wait() releases the lock, allowing other threads to enter synchronized block
  • Lock reacquisition: After notify(), the waiting thread must reacquire the lock before continuing

💡 Common Mistake: Using if instead of while with wait() can cause issues. Always use: while (!condition) { wait(); } to handle spurious wakeups and ensure the condition is still true after waking up.

Comparison: notify() vs notifyAll()

Method What It Does When to Use
notify() Wakes up one waiting thread (chosen randomly) When only one thread needs to proceed
notifyAll() Wakes up all waiting threads When all threads need notification or when you're unsure which thread to wake

Best Practice: When in doubt, use notifyAll() instead of notify(). It's safer as it prevents threads from being stuck waiting indefinitely, though it may have slightly more overhead.