One Lock can be associated with many Conditions. Lock is an "object", each condition is a "waiting set". This allows for independent conditions sharing critical section. For example, consider bounded producers-consumers problem. One way to solve it is to have one lock that protects the queue, and two independent waiting sets: one for producers, waiting for slot to place item in the queue, other for consumers waiting for items to take. Using plain old synchronized
and wait/notify
API, best we can do is along these lines:
This has the disadvantage of waking up both producers and consumers on every change to the queue, even if it cannot possibly allow given thread to proceed (e.g. consumer is awoken when some other consumer takes item from the queue). Using Lock/Condition API we can implement solution that separates waiting consumers and producers, and hence reduce redundant wakeups and checking:
Lock lock = new ReentrantLock();
Condition hasPlace = lock.newCondition();
Condition hasItems = lock.newCondition();
producer:
lock.lock();
try {
while (queue.isFull()) {
hasPlace.await();
}
queue.put(sth);
hasItems.signal();
} finally {
lock.unlock();
}
consumer:
lock.lock();
try {
while (queue.isEmpty()) {
hasItems.await();
}
product = queue.take();
hasPlace.signal();
} finally {
lock.unlock();
}
This way, consumer waits for producers to produce some item (hasItems condition), and upon removing an item from the queue it notifies producers that there is an empty slot (hasPlace condition). Both conditions are associated with the same critical section (Lock), so we keep the usual exclusion and release-lock-while-waiting guarantees, while gaining the ability to separate waiting queues.