Command Palette

Search for a command to run...

PHASE 13Advanced Java 5+ ~15 min· topic 5 of 11

Topic 13.5

Locks, Conditions and wait/notify

In one line

Threads often need to wait for something (an item in a queue, free space, a signal). The built-in way is wait/notifyAll on a synchronized object; since Java 5, ReentrantLock with Condition does the same with more control: timeouts, interruptible waiting, fairness and several separate wait queues.

Think of it like this

A bakery counter with a small shelf. The baker can't put bread on a full shelf, so they wait until a customer takes some. A customer can't take bread from an empty shelf, so they wait until the baker adds some. Each one, after acting, rings a bell so the waiting person checks the shelf again. The shelf is the shared state, the shop door key is the lock, and the bell is the signal.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

wait / notify
Built-in Object methods: wait releases the lock and sleeps until notified; notify wakes a waiting thread.
Spurious wake-up
A thread waking from wait without anyone calling notify. Allowed by the rules, so always re-check in a loop.
ReentrantLock
An explicit lock object with lock and unlock methods, plus extras like tryLock and timeouts.
Condition
A named waiting room attached to a lock, with await and signal.
Fair lock
A lock that hands itself to the thread that has waited longest. Fairer, but slower.
Producer-consumer
A pattern where some threads put work into a shared buffer and others take it out.
Bounded buffer
A queue with a maximum size, so fast producers must wait for consumers.

Step by step

01Waiting the old way: synchronized, wait, notifyAll

The consumer takes the lock, checks the condition in a while, and calls wait() if it can't proceed. The producer takes the same lock, changes the state, and calls notifyAll(). Notifying only marks waiters as ready; they compete for the lock after the notifier releases it.

Calling wait or notify without holding that object's lock throws IllegalMonitorStateException.

Shelf.javawhole filejava
synchronized void put(String bread) throws InterruptedException {
    while (items.size() == capacity) wait();     // shelf full: release the lock and wait
    items.add(bread);
    notifyAll();                                  // wake customers waiting for bread
}

synchronized String take() throws InterruptedException {
    while (items.isEmpty()) wait();               // nothing to take: wait
    String b = items.removeFirst();
    notifyAll();                                  // wake the baker waiting for space
    return b;
}

02Why while, not if

Two customers wait for bread. The baker adds one loaf and notifies both. Customer 1 gets the lock and takes the loaf. Customer 2 gets the lock next; with if, it would call removeFirst on an empty list and crash. With while, it re-checks, sees the shelf is empty, and waits again.

Why while, not ifdiagram
Rendering diagram…

03ReentrantLock: explicit lock and unlock

lock.lock(); try { ... } finally { lock.unlock(); } is the standard shape. Forgetting unlock (for example after an exception) leaves the lock held forever; finally prevents that. synchronized releases automatically, which is one reason to prefer it when you don't need the extras.

tryLock() returns false immediately if another thread holds the lock, so a thread can do something else instead of waiting. tryLock(1, SECONDS) waits at most a second, which is one way to avoid deadlocks (Topic 13.9).

04Conditions: separate waiting rooms

With one lock and two conditions, producers wait on notFull and consumers on notEmpty. A put signals notEmpty (only consumers wake), a take signals notFull (only producers wake). That's more precise than notifyAll(), which wakes everybody.

await() has the same rules as wait(): hold the lock, call it in a loop, and it releases the lock while waiting.

05Use the ready-made tools first

The bounded buffer above is exactly ArrayBlockingQueue (put blocks when full, take blocks when empty). One-off "wait until N things finished" is CountDownLatch; "at most N threads at once" is Semaphore (Topic 13.8).

Write your own wait/notify code only to learn, or when no library tool fits.

Try it yourself

  1. 1

    Change while to if

    In the first example, add a second customer thread and split the 10 items between the two customers (5 each), then change while (items.isEmpty()) to if. On a multi-core JDK you can get NoSuchElementException from removeFirst when two customers wake for one item.

  2. 2

    Make the buffer bigger

    In the Condition example, set capacity = 100. The answer is the same, but the producer rarely waits. Bounded buffers trade memory for smoothing out speed differences.

Code & diagrams

A bounded buffer with wait and notifyAll New tab
Sign in to run this example in your browser.

Expected output

customer took 10 items, total = 55
The same buffer with ReentrantLock and two Conditions Java 5+ New tab
Sign in to run this example in your browser.

Expected output

sum of 1..100 passed through a 2-slot buffer: 5050
tryLock: don't wait if the lock is busy Java 5+ New tab
Sign in to run this example in your browser.

Expected output

tryLock while another thread holds it: false
held by another thread? true
tryLock after release: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call wait without holding the lock

Remove synchronized from take() in the first example.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.wait0(Native Method)
...

Myth vs fact

Myth

A notified thread runs immediately.

Fact

It only becomes eligible; it must re-acquire the lock first, and by then the condition may have changed.

Myth

notify() is always enough and faster than notifyAll().

Fact

With different kinds of waiters, notify can wake the wrong one and stall the program. Use notifyAll or separate Conditions.

Myth

ReentrantLock is always faster than synchronized.

Fact

Modern JVMs make both similar. Choose ReentrantLock for its features, not speed.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    ReentrantLock is built on AbstractQueuedSynchronizer (AQS), a FIFO queue of parked threads plus one atomic state int. CountDownLatch, Semaphore and ReentrantReadWriteLock are all small AQS subclasses.

  • ▸

    Fair locks hand off strictly in arrival order, which avoids starvation but greatly reduces throughput (no "barging" by a running thread that could take the lock immediately). The default is non-fair.

  • ▸

    Condition.awaitNanos returns the remaining time, letting a loop wait with an overall deadline across spurious wake-ups; Object.wait(ms) can't tell you whether it timed out.

Remember this

  1. 1

    **wait()** may only be called while holding the object's lock (inside synchronized (obj)). It releases the lock and parks the thread until another thread calls notify()/notifyAll() on the same object (or it's interrupted, or a spurious wake-up happens). Before wait() returns, the thread re-acquires the lock.

  2. 2

    Always wait in a loop that re-checks the condition: while (queue.isEmpty()) lock.wait();. Threads can wake up spuriously (without a notify), and by the time a notified thread gets the lock, another thread may already have taken the item. An if instead of while is a classic bug.

  3. 3

    **notifyAll() wakes every waiting thread; notify()** wakes one arbitrary thread. With different kinds of waiters on one object (producers waiting for space, consumers waiting for items), notify() can wake the wrong kind and the program stalls. Prefer notifyAll() unless you're sure every waiter waits for the same thing.

  4. 4

    **ReentrantLock** (java.util.concurrent.locks, Java 5) is an explicit lock: lock() and unlock() in a try/finally so it's always released. It adds tryLock() (don't wait if busy), tryLock(timeout), lockInterruptibly(), and an optional fair mode (longest waiter goes first).

  5. 5

    **Condition** objects come from a lock (lock.newCondition()) and replace wait/notify: await(), signal(), signalAll(). One lock can have several conditions, such as notFull and notEmpty, so a producer signals exactly the consumers and vice versa. This is how ArrayBlockingQueue is built.

  6. 6

    **ReadWriteLock lets many readers in at once but writers alone; StampedLock** (Java 8) adds optimistic reads. In practice, reach for a ready-made tool first: BlockingQueue, CountDownLatch, Semaphore (Topic 13.8). Hand-written wait/notify code is easy to get subtly wrong.

Explain it without notes

01

Why must wait be called in a loop?

02

When would you choose ReentrantLock over synchronized?

03

What is the advantage of two Conditions over one notifyAll?

Practice

01

Rewrite the bakery example using ArrayBlockingQueue instead of hand-written wait/notify.

Trade-offs

  • ↔

    wait/notify needs no imports and works everywhere, but it's easy to misuse; Conditions are clearer and more precise.

  • ↔

    Explicit locks offer timeouts and fairness but must be unlocked in finally; forgetting is a serious bug synchronized can't have.

Done when you can

  • Done when you can write a correct wait loop and explain spurious wake-ups.

  • Done when you use ReentrantLock with try/finally and know tryLock and timeouts.

  • Done when you can build a bounded buffer with two Conditions.

  • Done when you reach for BlockingQueue, CountDownLatch or Semaphore before hand-written waiting.