A Condition is the explicit-lock equivalent of wait/notify/notifyAll, obtained from a Lock via newCondition(). Its decisive advantage: a single lock can have multiple Condition objects — multiple independent wait-sets — whereas an intrinsic monitor has exactly one. That lets you signal precisely the threads waiting on a specific predicate.
The bounded-buffer pattern
A classic producer/consumer needs two wait conditions: "buffer not full" for producers and "buffer not empty" for consumers. With synchronized you have one wait-set and must notifyAll(), waking everyone. With two Conditions you wake only the relevant group.
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int count, putIdx, takeIdx;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // releases lock, parks; re-acquires on wake
items[putIdx] = x;
putIdx = (putIdx + 1) % items.length;
count++;
notEmpty.signal(); // wake ONE consumer, not all
} finally {
lock.unlock();
}
}
How it improves on wait/notify
- Multiple wait-sets per lock — targeted signalling instead of waking unrelated threads (the "thundering herd" of
notifyAll). signal()is safe here because each Condition holds only threads waiting on the same predicate, so waking one is correct (unlike single-monitornotify(), which can wake the wrong waiter).- Richer waiting —
awaitNanos,await(time, unit),awaitUntil(deadline), andawaitUninterruptibly().
The rules carry over
You must hold the lock to call await/signal (else IllegalMonitorStateException). And always wait in a while loop, never an if — spurious wakeups exist, and even after a valid signal another thread may have changed the state before you re-acquire the lock. await() atomically releases the lock and re-acquires it on return.