When I first started learning about Java multithreading, one of the concepts that took me the longest to grasp was how threads can communicate with each other. Sure, I understood synchronized
blocks and how they help prevent race conditions—but I still didn’t know how to make one thread wait for another to finish a task or notify it when to continue.
In this article, I’ll walk you through inter-thread communication in Java using the Producer-Consumer problem, one of the most common and beginner-friendly examples.
🧠 What Is Inter-Thread Communication?
In a multi-threaded application, threads often need to coordinate their actions. Imagine a scenario where one thread produces data, and another consumes it. If the consumer tries to read data before it’s produced, we’ll get inconsistent or incorrect results.
Java provides built-in methods to help threads wait for certain conditions or notify others when they’re ready to proceed. These are:
1 2 3 4 5 | wait() notify() notifyAll() |
These methods are available to every object because they come from the Object
class, and they must be used inside a synchronized
block.
🧪 The Producer-Consumer Problem
Let me show you this with the classic Producer-Consumer problem:
- The Producer creates items (e.g., integers) and adds them to a shared object.
- The Consumer takes items from that shared object.
- The Consumer must wait until there is something to consume.
- The Producer must wait until the previous item is consumed.
🔧 Step 1: Without wait/notify (Incorrect Version)
When I first tried solving this, I used only synchronized
methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Q { int n; synchronized void put(int n) { this.n = n; System.out.println("Put: " + n); } synchronized int get() { System.out.println("Got: " + n); return n; } } |
Producer and Consumer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { int i = 0; while (true) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while (true) { q.get(); } } } |
And the main driver class:
1 2 3 4 5 6 7 8 9 | public class Driver { public static void main(String[] args) { Q q = new Q(); new Producer(q); new Consumer(q); } } |
❌ Problem:
This code runs, but you’ll see output like this:
1 2 3 4 5 6 7 8 9 | Put: 1 Put: 2 Put: 3 Got: 3 Got: 3 Got: 3 ... |
The Consumer ends up reading the same value multiple times or missing some entirely. This is because there’s no real synchronization on data availability—just mutual exclusion on the methods.
✅ Step 2: Using wait() and notify() (Correct Version)
Now let’s fix it using wait()
and notify()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Q { int n; boolean hasValue = false; synchronized void put(int n) { while (hasValue) { try { wait(); } catch (InterruptedException e) { } } this.n = n; System.out.println("Put: " + n); hasValue = true; notify(); } synchronized int get() { while (!hasValue) { try { wait(); } catch (InterruptedException e) { } } System.out.println("Got: " + n); hasValue = false; notify(); return n; } } |
The Producer
, Consumer
, and Driver
classes remain the same.
✅ Output:
1 2 3 4 5 6 7 8 9 | Put: 0 Got: 0 Put: 1 Got: 1 Put: 2 Got: 2 ... |