skip to content

How do Lock/Condition (await/signal/signalAll) improve on intrinsic wait/notify, and what does each map to?

level: principalimportance: should knowfreq 47%

answer

  1. await=wait, signal=notify, signalAll=notifyAll
  2. Multiple Conditions per lock = multiple wait-sets
  3. signal becomes safe because each Condition is one predicate
  4. Lock adds tryLock, lockInterruptibly, fairness
  5. Must unlock() in finally; await still needs a while loop

basics

~20 s

ReentrantLock with Condition objects is the modern replacement for synchronized + wait/notify. await() maps to wait(), signal() to notify(), and signalAll() to notifyAll(). The big win is that one Lock can have several Conditions (several wait-sets), so you can wake exactly the right group of threads instead of waking everyone.

solid answer

~40 s

A `ReentrantLock` plus its `Condition` objects is the explicit, more flexible analogue of intrinsic locking with wait/notify. `lock()`/`unlock()` replace entering/leaving a synchronized block; `condition.await()` replaces `wait()`, `condition.signal()` replaces `notify()`, and `condition.signalAll()` replaces `notifyAll()`. The decisive advantage is **multiple wait-sets per lock**: an intrinsic monitor has exactly one wait-set, so distinct conditions (e.g. notFull vs notEmpty) must share it, forcing notifyAll. With a Lock you create one Condition per predicate and signal only the relevant one, getting notify-level efficiency without lost wakeups. Lock also adds tryLock (with timeout), lockInterruptibly, and fair ordering. The cost is you MUST unlock in a finally block (synchronized releases automatically), and await still requires a while loop for spurious wakeups. You must hold the lock to call await/signal, just as you must hold the monitor for wait/notify.

code

java · 13 lines
java
ReentrantLock lock = new ReentrantLock();
Condition notFull  = lock.newCondition();
Condition notEmpty = lock.newCondition();

// producer
lock.lock();
try {
    while (isFull()) notFull.await();   // == obj.wait()
    enqueue(item);
    notEmpty.signal();                  // == obj.notify(), but only consumers
} finally {
    lock.unlock();                      // MUST be in finally
}

go deeper

for a junior

May know ReentrantLock exists as an alternative to synchronized but not the Condition mapping.

for a middle

Maps await/signal/signalAll to wait/notify/notifyAll and knows unlock must be in finally.

for a senior

Explains multiple Conditions per lock as the key advantage, plus tryLock/interruptibility/fairness, and writes a correct two-Condition buffer.

for a principal

Chooses the right primitive per situation, reasons about fairness vs throughput and starvation, mandates finally-unlock and guarded-await patterns, and prefers concurrent collections where they encapsulate the concern.

## Why a replacement exists Intrinsic synchronization (`synchronized` + `Object.wait/notify/notifyAll`) is simple but rigid. `java.util.concurrent.locks` (Java 5+) provides `Lock` and `Condition` as an **explicit** alternative with more control. Understanding the one-to-one mapping makes wait/notify knowledge transfer directly. ## The mapping | Intrinsic (`synchronized`) | Explicit (`ReentrantLock` + `Condition`) | |---|---| | `synchronized(obj) { }` | `lock.lock(); try { } finally { lock.unlock(); }` | | `obj.wait()` | `condition.await()` | | `obj.notify()` | `condition.signal()` | | `obj.notifyAll()` | `condition.signalAll()` | A `Condition` is obtained from a lock via `lock.newCondition()`. As with intrinsic monitors, you must **hold the lock** to call `await`/`signal`/`signalAll`, and `await` must sit inside a **while loop** (spurious wakeups apply identically). ## The headline improvement: multiple wait-sets per lock An object's intrinsic monitor has **exactly one wait-set**. So in a bounded buffer, producers (waiting for 'not full') and consumers (waiting for 'not empty') are forced into the *same* wait-set, and you must use `notifyAll()` to be safe — waking everyone even though only one group can proceed (the thundering herd). With a `ReentrantLock` you create **several Conditions on one lock**: ```java final ReentrantLock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); void put(T x) throws InterruptedException { lock.lock(); try { while (full()) notFull.await(); // own wait-set enqueue(x); notEmpty.signal(); // wake exactly a consumer } finally { lock.unlock(); } } T take() throws InterruptedException { lock.lock(); try { while (empty()) notEmpty.await(); T x = dequeue(); notFull.signal(); // wake exactly a producer } finally { lock.unlock(); } return x; } ``` Now `signal()` (one waiter) is **safe and efficient** because each Condition's wait-set holds only threads waiting on that one predicate. You get correctness *and* avoid the thundering herd — something intrinsic monitors cannot do. ## Other Lock capabilities - **`tryLock()` / `tryLock(time, unit)`** — attempt to acquire without blocking (or with a timeout), enabling deadlock-avoidance and back-off strategies. `synchronized` can only block indefinitely. - **`lockInterruptibly()`** — acquire the lock but abort if the thread is interrupted while waiting. - **Fairness** — `new ReentrantLock(true)` grants the lock in roughly FIFO order, reducing starvation (at a throughput cost). Intrinsic locks are unfair. - **Reentrancy** — like intrinsic locks, ReentrantLock lets the holding thread re-acquire. ## The trade-offs / costs - **Manual release.** `synchronized` releases the monitor automatically on block exit (even on exception). With `Lock` you **must** call `unlock()` in a `finally` block — forgetting it leaks the lock and deadlocks the system. This is the main footgun. - **More verbose / lower-level.** For a single simple condition, `synchronized` is shorter and harder to misuse. - **Spurious wakeups still apply** to `await()`, so the `while` loop is unchanged. ## When to choose which Default to `synchronized` for simple mutual exclusion. Reach for `ReentrantLock`/`Condition` when you need **multiple distinct conditions on one lock**, **timed or interruptible acquisition**, **fairness**, or `tryLock`-based deadlock avoidance. Above both, prefer the ready-made `BlockingQueue`/concurrent collections that encapsulate the whole pattern when they fit.

  • Why can signal() (vs signalAll()) be used safely with Conditions but notify() is risky with intrinsic monitors?
    Because each Condition is its own wait-set holding only threads waiting on that one predicate, so signaling one is guaranteed to wake a relevant waiter. An intrinsic monitor has a single wait-set mixing all predicates, so notify() may wake an irrelevant thread.
  • What is the single most dangerous difference when migrating from synchronized to Lock?
    Lock does not auto-release. You must call unlock() in a finally block; otherwise an exception or early return leaks the lock and deadlocks everything that needs it.

saying these in an interview costs you the question

  • Forgetting to unlock() in a finally block
  • Thinking an intrinsic monitor can have more than one wait-set
  • Believing await() is immune to spurious wakeups
  • Calling await/signal without holding the lock
  • Assuming ReentrantLock is always better than synchronized regardless of need

context