Java Concurrency

What is the difference between wait/notify and Condition?

wait/notify is tied to synchronized and intrinsic object monitors. Condition is tied to explicit locks like ReentrantLock and allows multiple named wait queues for clearer thread coordination.

ConcurrencyLocksThread Coordination

The Short Answer

wait/notify is the older monitor-based coordination mechanism built into every Java object.

Condition is the newer coordination mechanism used with explicit locks like ReentrantLock. It gives you more control, especially when different groups of threads are waiting for different reasons.

The Real Problem

Sometimes a thread cannot continue until some shared state changes. For example, a consumer cannot take from an empty queue, and a producer cannot put into a full queue.

The thread should not spin forever checking the condition. It should sleep, release the lock, and wake up when another thread changes the state.

Busy Waiting

while queue is empty
keep checking again and again
CPU wasted

The thread stays active even though it cannot make progress.

Waiting Properly

queue is empty
release lock and sleep
wake when signaled

The thread pauses safely and lets other threads modify the shared state.

Mental Model

Both mechanisms are trying to solve the same problem: a thread owns a lock, realizes the condition is not ready, releases the lock, waits, then wakes up and re-checks the condition.

1

Acquire lock

2

Check condition

3

Release lock + wait

4

Wake up

5

Re-check condition

The most important rule: always wait in a loop, not an if statement. A waiting thread can wake up even when the condition is still false.

wait/notify Example

With wait and notifyAll, the object itself is used as the monitor. You must be inside a synchronized block for that same object before calling them.

java
class SimpleBox {
    private final Object lock = new Object();
    private String value;

    public void put(String newValue) {
        synchronized (lock) {
            value = newValue;
            lock.notifyAll();
        }
    }

    public String take() throws InterruptedException {
        synchronized (lock) {
            while (value  == null) {
                // wait() is a blocking call
                // The thread:
                // - stops executing
                // - releases the monitor lock
                // - enters the WAITING state
                // - stays there until awakened
                // The thread blocks/sleeps, and does not burn CPU
                lock.wait();
            }

            String result = value;
            value = null;
            return result;
        }
    }
}

The consumer waits while the box is empty. The producer puts a value and notifies waiting threads.

Condition Example

With Condition, you use an explicit lock and create one or more condition queues from that lock.

java
class SimpleBox {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();

    private String value;

    public void put(String newValue) {
        lock.lock();
        try {
            value = newValue;
            // signalAll is the equivalent of the older notifyAll call
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String take() throws InterruptedException {
        lock.lock();
        try {
            while (value  == null) {
                // await is the equivalent of the older wait call
                notEmpty.await();
            }

            String result = value;
            value = null;
            return result;
        } finally {
            lock.unlock();
        }
    }
}

Conceptually, await is similar to wait, and signal/signalAll are similar to notify/notifyAll. The key difference is that Condition is tied to an explicit lock. Oracle’s documentation notes that await releases the associated lock and the thread must reacquire it before returning.

The Big Advantage of Condition

The major advantage is that one lock can have multiple condition queues.

This is perfect for a bounded queue. Producers wait when the queue is full. Consumers wait when the queue is empty. These are different reasons to wait.

wait/notify

One monitor wait set

producers + consumers waiting together

You may wake threads that cannot actually make progress.

Condition

Separate condition queues

notFull: producers wait here
notEmpty: consumers wait here

You can signal the group that is actually relevant.

Bounded Queue Example

This is where Condition really starts to make sense. Instead of one generic wait set, we create two named conditions: notFull and notEmpty.

java
class BoundedBuffer<T> {
    private final ReentrantLock lock = new ReentrantLock();

    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final Queue<T> queue = new ArrayDeque<>();
    private final int capacity;

    BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();
            }

            queue.add(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }

            T item = queue.remove();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

When a producer adds an item, it signals notEmpty because a consumer may now proceed. When a consumer removes an item, it signals notFull because a producer may now proceed.

Common Mistake: Using if Instead of while

This is wrong:

java
if (queue.isEmpty()) {
    notEmpty.await();
}

This is right:

java
while (queue.isEmpty()) {
    notEmpty.await();
}

The reason is that a thread can wake up and still not be allowed to proceed. Another thread may have taken the item first, or the wakeup may be spurious. The Java Condition API explicitly mentions spurious wakeups, so the condition must be re-checked after waking.

How to Say This in an Interview

wait/notify is tied to intrinsic locks and synchronized blocks. Condition is tied to explicit Lock implementations like ReentrantLock. The main benefit of Condition is that one lock can have multiple named wait queues, which makes coordination clearer and more precise than one generic monitor wait set.

Common Interview Follow-Ups

Is Condition a replacement for synchronized?

Not exactly. Condition works with explicit Lock objects, while wait/notify works with synchronized and intrinsic object monitors.

Should I use notify or notifyAll?

For interview-safe code, notifyAll is usually easier to reason about. notify can wake one arbitrary waiting thread, which may not be the one that can make progress.

Does await release the lock?

Yes. await releases the associated lock while waiting and reacquires it before returning.

Why does Condition help with producer-consumer?

Because producers and consumers can wait on separate conditions, such as notFull and notEmpty, instead of sharing one generic wait set.

Final Takeaway

Use wait/notify to understand the classic monitor model. Use Condition when you want clearer, more flexible coordination with explicit locks and multiple wait conditions.