- Master the wait/notify/notifyAll mechanism
- Understand why to use while loops with wait()
- Implement the producer-consumer pattern
- Know the modern alternatives in java.util.concurrent
Thread Communication: wait() and notify()
Synchronization prevents threads from interfering. But sometimes threads need to cooperate. Producer creates data, consumer processes it. Reader waits for writer. This needs thread communication—threads signaling each other.
The wait/notify Mechanism
Java provides three methods on every object for thread communication:
wait()- Releases the lock and waits for notificationnotify()- Wakes up one waiting threadnotifyAll()- Wakes up all waiting threads
Must be called from within a synchronized block on the same object.
Basic wait/notify Pattern
public class MessageQueue {
private String message;
private boolean hasMessage = false;
public synchronized void put(String msg) {
// Wait while there's already a message
while (hasMessage) {
try {
wait(); // Release lock and wait
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// Store the message
message = msg;
hasMessage = true;
// Notify waiting consumer
notify();
}
public synchronized String take() {
// Wait while there's no message
while (!hasMessage) {
try {
wait(); // Release lock and wait
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
// Get the message
String msg = message;
hasMessage = false;
// Notify waiting producer
notify();
return msg;
}
}
How wait() Works
When a thread calls wait():
- Releases the lock on the object
- Enters WAITING state
- Waits until another thread calls
notify()/notifyAll()on the same object - Re-acquires the lock before returning from
wait() - Continues execution after the
wait()call
synchronized (lock) {
// 1. Thread holds the lock here
while (!condition) {
lock.wait(); // 2. Releases lock, waits
// 4. Re-acquires lock when notified
}
// 5. Thread holds the lock again, condition is true
}
Why Use while Instead of if?
Always use while, never if:
// WRONG - using if
synchronized (lock) {
if (!condition) {
wait();
}
// Condition might be false again! (spurious wakeup)
}
// CORRECT - using while
synchronized (lock) {
while (!condition) {
wait();
}
// Guaranteed: condition is true
}
Reasons:
- Spurious wakeups: A thread may wake up without being notified
- Multiple waiters: Another thread might consume the condition first
- notifyAll(): All threads wake up, but only one should proceed
notify() vs notifyAll()
| Method | Behavior | Use When |
|---|---|---|
notify() |
Wakes one waiting thread (arbitrary) | Only one waiter can proceed |
notifyAll() |
Wakes all waiting threads | Multiple waiters might proceed, or conditions differ |
Best practice: Prefer notifyAll() for safety. It's less efficient but prevents subtle bugs.
// Use notifyAll when waiters have different conditions
public synchronized void finishWriting() {
writingComplete = true;
notifyAll(); // All readers should wake up
}
// notify() is OK when all waiters are equivalent
public synchronized void addItem(Item item) {
queue.add(item);
notify(); // Any one consumer can handle it
}
Producer-Consumer Example
A classic pattern where producers create items and consumers process them:
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int value) throws InterruptedException {
// Wait if queue is full
while (queue.size() == capacity) {
System.out.println("Queue full, producer waiting...");
wait();
}
queue.add(value);
System.out.println("Produced: " + value);
// Notify consumers
notifyAll();
}
public synchronized int consume() throws InterruptedException {
// Wait if queue is empty
while (queue.isEmpty()) {
System.out.println("Queue empty, consumer waiting...");
wait();
}
int value = queue.poll();
System.out.println("Consumed: " + value);
// Notify producers
notifyAll();
return value;
}
}
// Usage
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer(5);
// Producer thread
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
pc.produce(i);
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
pc.consume();
Thread.sleep(200); // Slower than producer
} catch (InterruptedException e) {
break;
}
}
});
producer.start();
consumer.start();
}
wait() with Timeout
You can specify a maximum wait time:
synchronized (lock) {
long deadline = System.currentTimeMillis() + 5000; // 5 seconds from now
while (!condition) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
throw new TimeoutException("Timed out waiting for condition");
}
wait(remaining);
}
}
Common Patterns
1. One-Shot Latch
A gate that opens once and stays open:
public class Latch {
private boolean open = false;
public synchronized void open() {
open = true;
notifyAll(); // Wake all waiting threads
}
public synchronized void await() throws InterruptedException {
while (!open) {
wait();
}
}
}
2. Barrier
Wait until N threads arrive:
public class SimpleBarrier {
private final int parties;
private int count = 0;
public SimpleBarrier(int parties) {
this.parties = parties;
}
public synchronized void await() throws InterruptedException {
count++;
if (count < parties) {
// Not everyone is here yet, wait
while (count < parties) {
wait();
}
} else {
// Everyone is here, wake them up
notifyAll();
}
}
}
3. Condition Variable
Wait for a specific condition:
public class BoundedBuffer<T> {
private final Object[] items;
private int count, putIndex, takeIndex;
public BoundedBuffer(int capacity) {
items = new Object[capacity];
}
public synchronized void put(T item) throws InterruptedException {
while (count == items.length) { // Buffer full
wait();
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
count++;
notifyAll();
}
@SuppressWarnings("unchecked")
public synchronized T take() throws InterruptedException {
while (count == 0) { // Buffer empty
wait();
}
T item = (T) items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
notifyAll();
return item;
}
}
Common Mistakes
1. Calling wait/notify Outside Synchronized Block
// WRONG - throws IllegalMonitorStateException
lock.wait();
// CORRECT
synchronized (lock) {
lock.wait();
}
2. Synchronizing on Different Objects
// WRONG - wait and notify on different locks
synchronized (lock1) {
lock2.wait(); // IllegalMonitorStateException!
}
// CORRECT - same object for synchronized and wait
synchronized (lock) {
lock.wait();
}
3. Using if Instead of while
// WRONG - may proceed when condition is false
if (!ready) wait();
// CORRECT - always rechecks condition
while (!ready) wait();
Modern Alternatives
Java provides higher-level abstractions in java.util.concurrent:
| Pattern | wait/notify | Modern Alternative |
|---|---|---|
| Latch | Custom implementation | CountDownLatch |
| Barrier | Custom implementation | CyclicBarrier |
| Queue | Manual synchronization | BlockingQueue |
| Condition | wait()/notify() |
Condition with Lock |
// Modern producer-consumer with BlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// Producer
queue.put(value); // Blocks if full
// Consumer
Integer value = queue.take(); // Blocks if empty
Thread communication: wait() releases lock and waits for notification, notify() wakes one waiting thread, notifyAll() wakes all. Must be called within synchronized block on the same object. Always use while to check conditions, never if. Prefer notifyAll() over notify() for safety. wait() releases the lock, returns after re-acquiring it. Consider java.util.concurrent classes for production code.
