1. The “Why”
Every Java object has an Intrinsic Lock (the monitor). Along with that lock, every object maintains a Wait Set—a list of threads that are suspended and waiting for a signal related to that object. This allows threads to communicate without using complex external libraries, using only the objects they are already synchronizing on.
2. Comparison: wait/notify vs. Condition Objects
| Feature | wait() / notify() |
Condition (await/signal) |
|---|---|---|
| Origin | Part of java.lang.Object (Available since JDK 1.0). |
Part of java.util.concurrent (Added in JDK 1.5). |
| Locking | Works with synchronized blocks. |
Works with ReentrantLock. |
| Wait Sets | Only one per object. | Multiple per lock (e.g., notFull, notEmpty). |
| Efficiency | notifyAll() wakes everyone, even if they can’t proceed. |
signal() can target specific groups of threads. |
3. The “Golden” Snippet: Classic Producer-Consumer
In this version, we don’t need a ReentrantLock. We use the synchronized keyword and the shared object itself to coordinate.
public class ClassicBuffer {
private final java.util.Queue<Integer> queue = new java.util.LinkedList<>();
private final int CAPACITY = 5;
public synchronized void produce(int item) throws InterruptedException {
// 1. Always check the condition in a WHILE loop
while (queue.size() == CAPACITY) {
// 2. Releases the 'this' lock and sleeps
wait();
}
queue.add(item);
System.out.println("Produced: " + item);
// 3. Wake up waiting consumers
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
// Releases the 'this' lock and sleeps
wait();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
// Wake up waiting producers
notifyAll();
return item;
}
}
Code Explanation:
synchronized: Both methods are synchronized onthis. A thread must own the object’s monitor to callwait()ornotify().wait(): When called, the thread is added to the object’s Wait Set and releases the lock. This allows other threads to enter and change the state (e.g., a Consumer to remove an item).notifyAll(): This wakes up all threads currently in the Wait Set. They all move to the “Blocked” state and fight to re-acquire the lock.- The Loop: Once a thread re-acquires the lock, it starts right after the
wait()call. Thewhileloop ensures it re-checks the condition (e.g., “is the queue still empty?”) before proceeding.
Example Output:
[Producer]: Queue full. Calling wait()...
[Consumer]: Removed item. Calling notifyAll()...
[Producer]: Waking up, re-checking while loop, adding item.
4. The Gotchas
- Spurious Wakeups: Occasionally, a thread wakes up without any
notify()being called (a quirk of OS/JVM interaction). This is why awhileloop is mandatory, never use anifstatement withwait(). - The
notify()Risk:notify()only wakes up one random thread. If you have multiple Producers and Consumers waiting,notify()might wake up another Producer when the queue is already full, leading to a “Deadlock-like” hang where everyone is sleeping. When in doubt, usenotifyAll(). - IllegalMonitorStateException: If you call
wait()outside of asynchronizedblock, your code will crash immediately.