Back to wait/notify, sleep/yield & Producer-Consumer
Q1Question 1 of 6

wait()/notify() vs Condition.await()/signal()?

Key Concepts
  • wait()/notify() are Object methods used inside synchronized blocks; each object has one wait-set.
  • Condition.await()/signal() are used with ReentrantLock; each lock can have multiple Conditions (multiple wait-sets).
  • With one wait-set, notify() may wake the wrong type of thread (a producer instead of a consumer).
  • Multiple Conditions allow targeted wake-ups: notEmpty.signal() wakes only consumers, notFull.signal() wakes only producers.
  • Both require the wait to be in a while loop due to spurious wake-ups and stolen conditions.
  • Condition also provides awaitUninterruptibly() and await(timeout, unit) for more precise control.

Definition

wait()/notify() uses a single wait-set per object (may wake wrong thread), while Condition.await()/signal() supports multiple wait-sets per lock for targeted wake-ups.
  • wait()/notify() – methods on Object, used inside synchronized. wait() releases the monitor lock and enters the object's single wait-set. notify() wakes one thread from that set. notifyAll() wakes all.
  • Condition – obtained via ReentrantLock.newCondition(). await() releases the lock and enters the Condition's wait-set. signal() wakes one thread from that specific Condition.
  • Single vs multiple wait-sets – with wait/notify, producers and consumers share one wait-set. notify() may wake a producer when only a consumer is needed. With Conditions, each type has its own wait-set.
  • Spurious wake-ups – both mechanisms can experience spurious wake-ups. Always use a while loop around wait() or await().

wait/notify is one waiting room for everyone. When you shout "NEXT", you might wake a withdrawal customer when only a deposit customer is needed. notifyAll() wakes all 50 people, but only 1 needs to act. Condition creates separate waiting rooms: deposit room and withdrawal room. Shout in the deposit room and only deposit customers wake up. Efficient and targeted.

Understanding the Concepts

How does wait()/notify() work?

JAVA
package com.bank.core;

import java.util.LinkedList;
import java.util.Queue;

public class WaitNotifyDemo {
    private final Queue<String> queue = new LinkedList<>();
    private final int capacity = 10;

    public synchronized void produce(String trade) throws InterruptedException {
        while (queue.size() >= capacity) {
            wait(); // releases lock, enters wait-set
        }
        queue.add(trade);
        notifyAll(); // wakes ALL waiters (producers AND consumers)
    }

    public synchronized String consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // releases lock, enters wait-set
        }
        String trade = queue.poll();
        notifyAll(); // wakes ALL waiters
        return trade;
    }
}

The problem: notifyAll() wakes both producers and consumers. If 50 consumers are waiting and one item is added, all 50 wake up, 49 recheck the condition, and go back to sleep. Wasteful.

How does Condition.await()/signal() improve this?

JAVA
package com.bank.core;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private final Queue<String> queue = new LinkedList<>();
    private final int capacity = 10;

    public void produce(String trade) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() >= capacity) notFull.await();
            queue.add(trade);
            notEmpty.signal(); // wakes ONE consumer only
        } finally { lock.unlock(); }
    }

    public String consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) notEmpty.await();
            String trade = queue.poll();
            notFull.signal(); // wakes ONE producer only
            return trade;
        } finally { lock.unlock(); }
    }
}

How do they compare?

| Feature | wait/notify | Condition |
|---|---|---|
| Lock type | synchronized | ReentrantLock |
| Wait-sets per lock | One | Multiple |
| Targeted wake-up | No (wakes any) | Yes (signal specific Condition) |
| Timeout | wait(millis) | await(time, unit) |
| Interruptible | Always | awaitUninterruptibly() option |
| Must hold lock | Yes (synchronized) | Yes (lock.lock()) |
| Spurious wake-ups | Yes (use while) | Yes (use while) |

What are the rules for both mechanisms?

  1. Must be called while holding the lock (synchronized for wait/notify, lock.lock() for Condition).
  2. Always use a while loop, never an if statement.
  3. wait() and await() release the lock. When they return, the lock is re-acquired.
  4. Prefer notifyAll() over notify() for wait/notify (safer). Use signal() with Condition (targeted).

What should a senior engineer know?

  • LinkedBlockingQueue internally uses two Conditions: one for notEmpty (consumers wait) and one for notFull (producers wait). It also uses two separate locks (takeLock and putLock) for concurrent put and take.
  • Condition.awaitNanos(long): returns the remaining time, allowing precise timeout tracking across retry loops.
  • In practice: use BlockingQueue (which handles all the locking and signaling internally) instead of writing your own wait/notify or Condition-based queue.

Visual Overview

DIAGRAM
  wait/notify (ONE wait-set):
  ┌─────────────────────────────────┐
  │ Object Monitor                   │
  │  Wait-Set: [P1, P2, C1, C2, C3] │  mixed!
  │  notify() wakes ANY one thread   │
  └─────────────────────────────────┘

  Condition (MULTIPLE wait-sets):
  ┌─────────────────────────────────┐
  │ ReentrantLock                    │
  │  notFull:  [P1, P2]             │  producers only
  │  notEmpty: [C1, C2, C3]         │  consumers only
  │  notFull.signal()  → wakes P1   │  targeted!
  │  notEmpty.signal() → wakes C1   │  targeted!
  └─────────────────────────────────┘

Key Takeaway

wait()/notify() are the classic thread communication mechanism, but they have a fundamental limitation: each object has only one wait-set. When producers and consumers share that set, notify() may wake the wrong type of thread, and notifyAll() wakes all threads wastefully.

Condition objects (from ReentrantLock.newCondition()) solve this by providing multiple wait-sets per lock. notEmpty.signal() wakes only a consumer. notFull.signal() wakes only a producer. Zero wasted wake-ups.

Remember one sentence: use Condition for targeted wake-ups with separate wait-sets per condition, and always wrap await() in a while loop.

---

Share
Use ← → arrow keys to navigate