Java Concurrency — Inter-Thread Communication (Wait/Notify)
"
wait()andnotify()are the heartbeat of the Java monitor — the mechanism by which threads cooperate instead of just competing."
This article covers how threads communicate with each other: wait()/notify(), Condition variables, and the classic printing problems that teach these fundamentals.
The Need for Communication
Locks prevent threads from interfering with each other. But sometimes threads need to cooperate: "I've produced data — you can consume it now" or "the buffer is full — wait before producing more."
wait(), notify(), notifyAll()
These methods are defined on Object (not Thread), because they operate on the object's monitor — the same monitor used by synchronized.
The Contract
synchronized (lock) {
while (!condition) {
lock.wait(); // Release monitor, enter wait set, sleep
}
// Condition is true — proceed
}
synchronized (lock) {
// Change state that affects the condition
condition = true;
lock.notify(); // Wake up ONE waiting thread
// or
lock.notifyAll(); // Wake up ALL waiting threads
}
How wait/notify Works
Rules (MUST Follow)
Must hold the monitor —
wait()/notify()must be called insidesynchronizedon the same objectAlways loop on condition — never use
if, always usewhile(spurious wakeups)Prefer
notifyAll()—notify()wakes only one thread; withnotifyAll(), all waiters re-check
// ❌ WRONG — wait() outside synchronized
lock.wait(); // 💥 IllegalMonitorStateException
// ❌ WRONG — if instead of while
synchronized (lock) {
if (!ready) lock.wait(); // May proceed even if still not ready!
}
// ✅ CORRECT
synchronized (lock) {
while (!ready) {
lock.wait();
}
// Process...
}
The Spurious Wakeup Problem
A thread can wake from wait() without being notified. This is a real OS-level behavior (POSIX allows it). That's why the while-loop is mandatory:
// ❌ Vulnerable to spurious wakeup
synchronized (lock) {
if (buffer.isEmpty()) { // CHECK once
lock.wait(); // May wake without notify!
}
consume(buffer.remove()); // 💥 May fail — buffer still empty!
}
// ✅ Protected against spurious wakeup
synchronized (lock) {
while (buffer.isEmpty()) { // RE-CHECK after every wakeup
lock.wait();
}
consume(buffer.remove()); // ✅ Guaranteed non-empty
}
Condition Variables — The Modern Alternative
Condition (from java.util.concurrent.locks) is the modern replacement for wait()/notify(). Major advantage: multiple conditions per lock.
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
wait/notify vs Condition
Object (Monitors) |
Condition (Locks) |
|---|---|
wait() |
await() |
wait(timeout) |
await(timeout, unit) |
notify() |
signal() |
notifyAll() |
signalAll() |
| One wait-set per monitor | Multiple conditions per lock |
Why Multiple Conditions Matter
Thread.join() — Waiting for Completion
join() makes the current thread wait until the target thread finishes:
Thread worker = new Thread(() -> {
// Long computation
computeResult();
});
worker.start();
worker.join(); // Main thread blocks here until worker finishes
System.out.println("Worker is done!");
join() with Timeout
worker.join(5000); // Wait at most 5 seconds
if (worker.isAlive()) {
System.out.println("Worker still running after 5s — proceeding without it");
}
Problem: Printing Odd/Even with Two Threads
Goal: Thread-A prints odd numbers, Thread-B prints even numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 — alternating.
Solution using wait/notify
public class OddEvenPrinter {
private final int limit;
private int current = 1;
private final Object lock = new Object();
public OddEvenPrinter(int limit) {
this.limit = limit;
}
public void printOdd() {
synchronized (lock) {
while (current <= limit) {
while (current % 2 == 0 && current <= limit) {
try { lock.wait(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
if (current <= limit) {
System.out.println(Thread.currentThread().getName() + ": " + current);
current++;
lock.notify();
}
}
}
}
public void printEven() {
synchronized (lock) {
while (current <= limit) {
while (current % 2 == 1 && current <= limit) {
try { lock.wait(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
if (current <= limit) {
System.out.println(Thread.currentThread().getName() + ": " + current);
current++;
lock.notify();
}
}
}
}
public static void main(String[] args) {
OddEvenPrinter printer = new OddEvenPrinter(10);
Thread oddThread = new Thread(printer::printOdd, "Odd-Thread");
Thread evenThread = new Thread(printer::printEven, "Even-Thread");
oddThread.start();
evenThread.start();
}
}
Output:
Odd-Thread: 1
Even-Thread: 2
Odd-Thread: 3
Even-Thread: 4
...
Even-Thread: 10
Problem: Print N Words Using N Threads in Order
Goal: Given ["Hello", "World", "Foo", "Bar"], use 4 threads where Thread-0 prints "Hello", Thread-1 prints "World", etc. — cycling repeatedly.
Solution using Condition Variables
public class OrderedWordPrinter {
private final String[] words;
private final int numThreads;
private int currentTurn = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition[] conditions;
public OrderedWordPrinter(String[] words) {
this.words = words;
this.numThreads = words.length;
this.conditions = new Condition[numThreads];
for (int i = 0; i < numThreads; i++) {
conditions[i] = lock.newCondition();
}
}
public void printWord(int threadId, int totalRounds) {
for (int round = 0; round < totalRounds; round++) {
lock.lock();
try {
while (currentTurn != threadId) {
conditions[threadId].await();
}
System.out.println("Thread-" + threadId + ": " + words[threadId]);
currentTurn = (currentTurn + 1) % numThreads;
conditions[currentTurn].signal(); // Wake the NEXT thread only
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
String[] words = {"Hello", "World", "Foo", "Bar"};
OrderedWordPrinter printer = new OrderedWordPrinter(words);
for (int i = 0; i < words.length; i++) {
final int id = i;
new Thread(() -> printer.printWord(id, 3)).start();
}
}
}
Output:
Thread-0: Hello
Thread-1: World
Thread-2: Foo
Thread-3: Bar
Thread-0: Hello
Thread-1: World
...
Key insight: Using separate
Conditionper thread means we only wake the exact thread that should run next — no wasted wakeups.
Problem: Interleaved Series Printing
Goal: Two threads — one prints Fibonacci, one prints AP (arithmetic progression) — alternating outputs: fib_0, ap_0, fib_1, ap_1, fib_2, ap_2, ...
Solution
public class InterleavedSeriesPrinter {
private final Object lock = new Object();
private boolean fibTurn = true;
public void printFibonacci(int count) {
long a = 0, b = 1;
for (int i = 0; i < count; i++) {
synchronized (lock) {
while (!fibTurn) {
try { lock.wait(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
System.out.println("Fib[" + i + "]: " + a);
long temp = a + b;
a = b;
b = temp;
fibTurn = false;
lock.notify();
}
}
}
public void printAP(int count, int start, int diff) {
int value = start;
for (int i = 0; i < count; i++) {
synchronized (lock) {
while (fibTurn) {
try { lock.wait(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
System.out.println("AP[" + i + "]: " + value);
value += diff;
fibTurn = true;
lock.notify();
}
}
}
public static void main(String[] args) {
InterleavedSeriesPrinter printer = new InterleavedSeriesPrinter();
new Thread(() -> printer.printFibonacci(5)).start();
new Thread(() -> printer.printAP(5, 2, 3)).start();
}
}
Output:
Fib[0]: 0
AP[0]: 2
Fib[1]: 1
AP[1]: 5
Fib[2]: 1
AP[2]: 8
Fib[3]: 2
AP[3]: 11
Fib[4]: 3
AP[4]: 14
Common Pitfalls
Pitfall 1: wait() Without synchronized
// ❌ Will throw IllegalMonitorStateException
lock.wait();
// ✅ Must be inside synchronized on the same object
synchronized (lock) {
lock.wait();
}
Pitfall 2: Using if Instead of while with wait()
// ❌ Spurious wakeup → condition may still be false
synchronized (lock) {
if (!ready) lock.wait();
process(); // May fail!
}
// ✅ Always re-check condition
synchronized (lock) {
while (!ready) lock.wait();
process(); // Guaranteed ready
}
Pitfall 3: notify() vs notifyAll()
// ❌ notify() may wake the WRONG thread
// If producers AND consumers both wait on the same lock,
// notify() might wake another producer instead of a consumer
lock.notify();
// ✅ notifyAll() wakes everyone — they re-check their conditions
lock.notifyAll();
// ✅ Or better: use separate Conditions per role
notEmpty.signal(); // Wake a consumer specifically
Pitfall 4: Forgetting to notify After State Change
synchronized (lock) {
data = produceData();
ready = true;
// ❌ Forgot lock.notify() — consumers wait forever!
}
FAQs
Q1: Why must wait() be called inside a synchronized block?
A: wait() releases the monitor and puts the thread in the wait set. If the thread doesn't hold the monitor, there's nothing to release — that's why it throws IllegalMonitorStateException. The synchronized block also ensures that the condition check and the wait() call happen atomically.
Q2: Why use while instead of if when calling wait()?
A: Two reasons: (1) Spurious wakeups — the OS can wake a thread without notify() being called. (2) Condition not yet met — with notifyAll(), multiple threads wake up, but only one should proceed. The while loop makes each re-check the condition.
Q3: What is the difference between notify() and notifyAll()?
A: notify() wakes one arbitrary thread from the wait set. notifyAll() wakes all threads. In general, prefer notifyAll() because notify() can cause livelock if it wakes the wrong thread. Use notify() only when all waiting threads are equivalent.
Q4: How does Condition differ from wait()/notify()?
A: Condition supports multiple wait-sets per lock. With wait()/notify(), all threads wait on the same object's single wait-set. Condition lets producers wait on notFull and consumers on notEmpty — enabling precise signaling.
Q5: What happens if you call notify() and no thread is waiting?
A: Nothing. The signal is lost. Unlike CountDownLatch or Semaphore, there's no "memory" of notifications. If a thread calls wait() after the notify(), it will still block.