skip to content

What is a Condition obtained from a Lock, and why can it be better than wait()/notify() on a monitor?

level: seniorimportance: must knowfreq 62%

answer

  1. Condition = wait-set on a Lock; await/signal/signalAll ↔ wait/notify/notifyAll
  2. Many Conditions per lock → notFull + notEmpty, targeted signaling
  3. await() atomically releases lock + suspends, re-acquires on wake
  4. ALWAYS await in a while loop (spurious wakeups, signal/re-acquire gap)
  5. Must hold the lock or IllegalMonitorStateException; prefer BlockingQueue when it fits

basics

~20 s

A Condition is a waiting area tied to a Lock, created with lock.newCondition(). Threads call await() to wait and signal()/signalAll() to wake them — like wait()/notify() but you can have several separate waiting areas per lock, so you can wake exactly the right group of waiters.

solid answer

~50 s

A Condition is the explicit-lock analogue of an object's wait/notify mechanism. You get one with lock.newCondition(); a waiting thread calls condition.await() (which atomically releases the lock and suspends), and another thread calls condition.signal() or signalAll() to wake waiters. The key advantage over the single wait-set every object has is that one Lock can own multiple Conditions — for a bounded buffer you make a notFull and a notEmpty condition, so a producer that frees space signals only notFull and consumers waiting on notEmpty aren't needlessly woken. This is more precise and avoids the 'wake everyone, most go back to sleep' waste of notifyAll. The rules mirror monitors: you must hold the lock to call await/signal (else IllegalMonitorStateException), and you must await inside a while loop that re-checks the condition because of spurious wakeups and the gap between signal and re-acquisition.

code

java · 24 lines
java
final Lock lock = new ReentrantLock();
final Condition notFull  = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] buf = new Object[16];
int count, head, tail;

void put(Object x) throws InterruptedException {
    lock.lock();
    try {
        while (count == buf.length) notFull.await();   // wait for space
        buf[tail] = x; tail = (tail + 1) % buf.length; count++;
        notEmpty.signal();                              // wake one consumer
    } finally { lock.unlock(); }
}

Object take() throws InterruptedException {
    lock.lock();
    try {
        while (count == 0) notEmpty.await();            // wait for data
        Object x = buf[head]; head = (head + 1) % buf.length; count--;
        notFull.signal();                               // wake one producer
        return x;
    } finally { lock.unlock(); }
}

go deeper

for a junior

Recognizes that await()/signal() are like wait()/notify() and that a Condition comes from a Lock; can read a producer/consumer example.

for a middle

Writes await inside a while loop, holds the lock around await/signal, and explains that await releases the lock while suspended.

for a senior

Designs with multiple Conditions (notFull/notEmpty) for targeted signaling, chooses signal vs signalAll deliberately, and explains the spurious-wakeup and signal/re-acquire rationale for the loop.

for a principal

Reasons about the AQS condition-queue mechanics, missed-signal and lost-wakeup hazards, fairness interactions, and when to drop down to raw Conditions versus using BlockingQueue/Phaser/other higher-level synchronizers.

## The coordination problem Sometimes a thread must **wait for a condition to become true** before proceeding — a consumer can't take from an empty queue; a producer can't add to a full one. Busy-waiting (looping and checking) wastes CPU. We want the thread to *sleep* until another thread signals that the situation changed. ## The monitor baseline: wait / notify With `synchronized`, every object has one hidden **wait-set**. Inside a synchronized block you call: - `obj.wait()` — atomically release the monitor and suspend, joining the wait-set. - `obj.notify()` — wake **one** arbitrary waiter; `obj.notifyAll()` — wake **all**. The limitation: **one wait-set per object**. If producers and consumers wait on the *same* object, you can't selectively wake only the producers or only the consumers. The safe-but-wasteful workaround is `notifyAll()` — wake everyone and let those whose condition still isn't met go back to sleep. ## Conditions: multiple wait-sets per lock `ReentrantLock` (and any `Lock`) lets you create as many wait-sets as you like: ```java final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); ``` A `Condition` mirrors the monitor methods, renamed to avoid clashing with `Object`'s: - `await()` ↔ `wait()` — atomically releases the lock and suspends on *this* condition's wait-set. - `signal()` ↔ `notify()` — wakes one waiter on this condition. - `signalAll()` ↔ `notifyAll()` — wakes all waiters on this condition. Now a producer that frees a slot signals **only** `notFull`; consumers parked on `notEmpty` are left undisturbed. This **targeted signaling** is more efficient and clearer than one shared wait-set. ## The two non-negotiable rules **Rule 1 — hold the lock.** You must own the lock when calling `await`/`signal`/`signalAll`, exactly as `wait`/`notify` require holding the monitor. Otherwise you get `IllegalMonitorStateException`. `await()` atomically releases the lock as it suspends, and re-acquires it before returning — so on return you again hold the lock. **Rule 2 — always wait in a loop, never an `if`.** Re-check the predicate in a `while` after `await()` returns, for three reasons: 1. **Spurious wakeups** — a thread can wake without any signal (allowed by the spec). 2. **The signal/re-acquire gap** — between being signaled and re-acquiring the lock, another thread may have run and invalidated the condition (e.g. taken the item you were signaled about). 3. **`signalAll`** — wakes many threads; only one can proceed, the rest must recheck and re-wait. ```java Object take() throws InterruptedException { lock.lock(); try { while (count == 0) // WHILE, not if notEmpty.await(); // releases lock, suspends, re-acquires on wake Object x = items[head]; count--; notFull.signal(); // wake exactly one producer return x; } finally { lock.unlock(); } } ``` ## Bounded-buffer producer/consumer (the canonical example) ```java void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // wait for space items[tail] = x; count++; notEmpty.signal(); // wake a consumer } finally { lock.unlock(); } } ``` Two conditions on one lock give each role its own wait-set, so signals are precise. ## signal vs signalAll `signal()` (wake one) is cheaper but only safe when **any** waiter can make progress and you wake the right wait-set; `signalAll()` is the safe default when waiters guard different sub-conditions on the *same* condition object. Separate `Condition`s often let you safely use `signal()` because each wait-set is homogeneous. ## Extra abilities over wait/notify `await` has timed (`await(time, unit)`, `awaitNanos`) and uninterruptible (`awaitUninterruptibly`) variants and a deadline form (`awaitUntil`) — finer control than plain `wait`. Internally each `Condition` is a separate FIFO wait queue on the lock's AQS. ## When you don't need any of this For a plain bounded producer/consumer, prefer a ready-made `BlockingQueue` (e.g. `ArrayBlockingQueue`) — it implements exactly this Condition logic correctly so you don't hand-roll it. Reach for raw `Condition` only for custom coordination the library doesn't cover.

  • Why must await() be inside a while loop rather than an if?
    Three reasons: spurious wakeups (a thread may wake with no signal), the gap between being signaled and re-acquiring the lock (another thread can invalidate the predicate in between), and signalAll waking many threads where only one can proceed. The while re-checks the predicate after every wake, so the thread only proceeds when the condition is genuinely true.
  • How does using two Conditions improve on a single object's wait/notify for a bounded buffer?
    An object has one wait-set, so producers and consumers waiting on it can't be woken selectively — you must notifyAll and let the wrong group go back to sleep. With notFull and notEmpty Conditions, a producer freeing space signals only notFull and a consumer adding data signals only notEmpty, so each signal wakes exactly the relevant waiters: less wasted wakeup churn and clearer intent.

saying these in an interview costs you the question

  • Using if instead of while around await() — breaks on spurious wakeups and the signal/re-acquire race
  • Calling await/signal without holding the lock (IllegalMonitorStateException)
  • Claiming one object can have multiple wait-sets via wait/notify — it has exactly one; multiple wait-sets need Conditions
  • Saying await() does not release the lock — it does, atomically, while suspended
  • Hand-rolling a bounded buffer when ArrayBlockingQueue already does it correctly

context