Java Concurrency
What is the difference between wait/notify and Condition?
wait/notify is tied to synchronized and intrinsic object monitors. Condition is tied to explicit locks like ReentrantLock and allows multiple named wait queues for clearer thread coordination.
The Short Answer
wait/notify is the older monitor-based coordination mechanism built into every Java object.
Condition is the newer coordination mechanism used with explicit locks like ReentrantLock. It gives you more control, especially when different groups of threads are waiting for different reasons.
The Real Problem
Sometimes a thread cannot continue until some shared state changes. For example, a consumer cannot take from an empty queue, and a producer cannot put into a full queue.
The thread should not spin forever checking the condition. It should sleep, release the lock, and wake up when another thread changes the state.
Busy Waiting
The thread stays active even though it cannot make progress.
Waiting Properly
The thread pauses safely and lets other threads modify the shared state.
Mental Model
Both mechanisms are trying to solve the same problem: a thread owns a lock, realizes the condition is not ready, releases the lock, waits, then wakes up and re-checks the condition.
Acquire lock
Check condition
Release lock + wait
Wake up
Re-check condition
wait/notify Example
With wait and notifyAll, the object itself is used as the monitor. You must be inside a synchronized block for that same object before calling them.
class SimpleBox {
private final Object lock = new Object();
private String value;
public void put(String newValue) {
synchronized (lock) {
value = newValue;
lock.notifyAll();
}
}
public String take() throws InterruptedException {
synchronized (lock) {
while (value == null) {
// wait() is a blocking call
// The thread:
// - stops executing
// - releases the monitor lock
// - enters the WAITING state
// - stays there until awakened
// The thread blocks/sleeps, and does not burn CPU
lock.wait();
}
String result = value;
value = null;
return result;
}
}
}The consumer waits while the box is empty. The producer puts a value and notifies waiting threads.
Condition Example
With Condition, you use an explicit lock and create one or more condition queues from that lock.
class SimpleBox {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private String value;
public void put(String newValue) {
lock.lock();
try {
value = newValue;
// signalAll is the equivalent of the older notifyAll call
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public String take() throws InterruptedException {
lock.lock();
try {
while (value == null) {
// await is the equivalent of the older wait call
notEmpty.await();
}
String result = value;
value = null;
return result;
} finally {
lock.unlock();
}
}
}Conceptually, await is similar to wait, and signal/signalAll are similar to notify/notifyAll. The key difference is that Condition is tied to an explicit lock. Oracle’s documentation notes that await releases the associated lock and the thread must reacquire it before returning.
The Big Advantage of Condition
The major advantage is that one lock can have multiple condition queues.
This is perfect for a bounded queue. Producers wait when the queue is full. Consumers wait when the queue is empty. These are different reasons to wait.
wait/notify
One monitor wait set
You may wake threads that cannot actually make progress.
Condition
Separate condition queues
You can signal the group that is actually relevant.
Bounded Queue Example
This is where Condition really starts to make sense. Instead of one generic wait set, we create two named conditions: notFull and notEmpty.
class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.add(item);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
T item = queue.remove();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}When a producer adds an item, it signals notEmpty because a consumer may now proceed. When a consumer removes an item, it signals notFull because a producer may now proceed.
Common Mistake: Using if Instead of while
This is wrong:
if (queue.isEmpty()) {
notEmpty.await();
}This is right:
while (queue.isEmpty()) {
notEmpty.await();
}The reason is that a thread can wake up and still not be allowed to proceed. Another thread may have taken the item first, or the wakeup may be spurious. The Java Condition API explicitly mentions spurious wakeups, so the condition must be re-checked after waking.
How to Say This in an Interview
Common Interview Follow-Ups
Is Condition a replacement for synchronized?
Not exactly. Condition works with explicit Lock objects, while wait/notify works with synchronized and intrinsic object monitors.
Should I use notify or notifyAll?
For interview-safe code, notifyAll is usually easier to reason about. notify can wake one arbitrary waiting thread, which may not be the one that can make progress.
Does await release the lock?
Yes. await releases the associated lock while waiting and reacquires it before returning.
Why does Condition help with producer-consumer?
Because producers and consumers can wait on separate conditions, such as notFull and notEmpty, instead of sharing one generic wait set.