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.
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.
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
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())toif. On a multi-core JDK you can getNoSuchElementExceptionfromremoveFirstwhen two customers wake for one item. - 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
Expected output
customer took 10 items, total = 55Expected output
sum of 1..100 passed through a 2-slot buffer: 5050Expected output
tryLock while another thread holds it: false
held by another thread? true
tryLock after release: trueBreak 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.
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.
- ▸
ReentrantLockis built onAbstractQueuedSynchronizer(AQS), a FIFO queue of parked threads plus one atomicstateint.CountDownLatch,SemaphoreandReentrantReadWriteLockare 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.awaitNanosreturns 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
**
wait()** may only be called while holding the object's lock (insidesynchronized (obj)). It releases the lock and parks the thread until another thread callsnotify()/notifyAll()on the same object (or it's interrupted, or a spurious wake-up happens). Beforewait()returns, the thread re-acquires the lock. - 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. Anifinstead ofwhileis a classic bug. - 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. PrefernotifyAll()unless you're sure every waiter waits for the same thing. - 4
**
ReentrantLock** (java.util.concurrent.locks, Java 5) is an explicit lock:lock()andunlock()in atry/finallyso it's always released. It addstryLock()(don't wait if busy),tryLock(timeout),lockInterruptibly(), and an optional fair mode (longest waiter goes first). - 5
**
Condition** objects come from a lock (lock.newCondition()) and replacewait/notify:await(),signal(),signalAll(). One lock can have several conditions, such asnotFullandnotEmpty, so a producer signals exactly the consumers and vice versa. This is howArrayBlockingQueueis built. - 6
**
ReadWriteLocklets 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-writtenwait/notifycode is easy to get subtly wrong.
Explain it without notes
Why must wait be called in a loop?
When would you choose ReentrantLock over synchronized?
What is the advantage of two Conditions over one notifyAll?
Practice
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.