Definition
- wait()/notify() – methods on
Object, used insidesynchronized.wait()releases the monitor lock and enters the object's single wait-set.notify()wakes one thread from that set.notifyAll()wakes all. - Condition – obtained via
ReentrantLock.newCondition().await()releases the lock and enters the Condition's wait-set.signal()wakes one thread from that specific Condition. - Single vs multiple wait-sets – with wait/notify, producers and consumers share one wait-set.
notify()may wake a producer when only a consumer is needed. With Conditions, each type has its own wait-set. - Spurious wake-ups – both mechanisms can experience spurious wake-ups. Always use a
whileloop aroundwait()orawait().
wait/notify is one waiting room for everyone. When you shout "NEXT", you might wake a withdrawal customer when only a deposit customer is needed. notifyAll() wakes all 50 people, but only 1 needs to act. Condition creates separate waiting rooms: deposit room and withdrawal room. Shout in the deposit room and only deposit customers wake up. Efficient and targeted.
Understanding the Concepts
How does wait()/notify() work?
package com.bank.core;
import java.util.LinkedList;
import java.util.Queue;
public class WaitNotifyDemo {
private final Queue<String> queue = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(String trade) throws InterruptedException {
while (queue.size() >= capacity) {
wait(); // releases lock, enters wait-set
}
queue.add(trade);
notifyAll(); // wakes ALL waiters (producers AND consumers)
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // releases lock, enters wait-set
}
String trade = queue.poll();
notifyAll(); // wakes ALL waiters
return trade;
}
}
The problem: notifyAll() wakes both producers and consumers. If 50 consumers are waiting and one item is added, all 50 wake up, 49 recheck the condition, and go back to sleep. Wasteful.
How does Condition.await()/signal() improve this?
package com.bank.core;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Queue<String> queue = new LinkedList<>();
private final int capacity = 10;
public void produce(String trade) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= capacity) notFull.await();
queue.add(trade);
notEmpty.signal(); // wakes ONE consumer only
} finally { lock.unlock(); }
}
public String consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
String trade = queue.poll();
notFull.signal(); // wakes ONE producer only
return trade;
} finally { lock.unlock(); }
}
}
How do they compare?
| Feature | wait/notify | Condition |
|---|---|---|
| Lock type | synchronized | ReentrantLock |
| Wait-sets per lock | One | Multiple |
| Targeted wake-up | No (wakes any) | Yes (signal specific Condition) |
| Timeout | wait(millis) | await(time, unit) |
| Interruptible | Always | awaitUninterruptibly() option |
| Must hold lock | Yes (synchronized) | Yes (lock.lock()) |
| Spurious wake-ups | Yes (use while) | Yes (use while) |
What are the rules for both mechanisms?
- Must be called while holding the lock (synchronized for wait/notify, lock.lock() for Condition).
- Always use a
whileloop, never anifstatement. wait()andawait()release the lock. When they return, the lock is re-acquired.- Prefer
notifyAll()overnotify()for wait/notify (safer). Usesignal()with Condition (targeted).
What should a senior engineer know?
- LinkedBlockingQueue internally uses two Conditions: one for
notEmpty(consumers wait) and one fornotFull(producers wait). It also uses two separate locks (takeLockandputLock) for concurrent put and take. - Condition.awaitNanos(long): returns the remaining time, allowing precise timeout tracking across retry loops.
- In practice: use
BlockingQueue(which handles all the locking and signaling internally) instead of writing your own wait/notify or Condition-based queue.
Visual Overview
wait/notify (ONE wait-set): ┌─────────────────────────────────┐ │ Object Monitor │ │ Wait-Set: [P1, P2, C1, C2, C3] │ mixed! │ notify() wakes ANY one thread │ └─────────────────────────────────┘ Condition (MULTIPLE wait-sets): ┌─────────────────────────────────┐ │ ReentrantLock │ │ notFull: [P1, P2] │ producers only │ notEmpty: [C1, C2, C3] │ consumers only │ notFull.signal() → wakes P1 │ targeted! │ notEmpty.signal() → wakes C1 │ targeted! └─────────────────────────────────┘
Key Takeaway
wait()/notify() are the classic thread communication mechanism, but they have a fundamental limitation: each object has only one wait-set. When producers and consumers share that set, notify() may wake the wrong type of thread, and notifyAll() wakes all threads wastefully.
Condition objects (from ReentrantLock.newCondition()) solve this by providing multiple wait-sets per lock. notEmpty.signal() wakes only a consumer. notFull.signal() wakes only a producer. Zero wasted wake-ups.
Remember one sentence: use Condition for targeted wake-ups with separate wait-sets per condition, and always wrap await() in a while loop.
---