wait and notify
guarded blocks, spurious wakeups, and why you should use something else.
- chapter
- chapter 09.04
- reading time
- 8 min read
wait() lets a thread inside a synchronized block give up the monitor and sleep until some condition might be true. notify() / notifyAll() wake sleepers up. the rules: only call them while holding the monitor, always wait in a while loop that re-checks the condition, and prefer notifyAll(). then, honestly, use java.util.concurrent instead.
the picture
back to the one-chair room. sometimes the thread inside can't make progress: it wants to take from a buffer that's empty. it can't just sit in the chair waiting (nobody else could get in to fill the buffer), and it can't keep walking in and out checking (busy-waiting burns a CPU core). so it steps into a side room (the wait set), gives up the chair, and asks to be poked when something changes.
poking is notify(). the poked thread doesn't jump back into the chair, though. it rejoins the queue at the door (the entry set) and has to win the monitor back like everyone else. by the time it does, the world may have changed again. that's the entire chapter, really.
nobody owns the monitor. any thread that enters synchronized (lock) gets it straight away.
three things to notice in there:
wait()releases the monitor completely, even if the thread had entered it three times. that's the only way anyone else can get in to change the condition.notify()doesn't release anything. the notifier keeps the monitor until it exits its block.- notify with nobody in the wait set does nothing at all. notifications aren't stored. a thread that starts waiting after the notify sleeps until the next one.
the guarded block
the pattern is always the same: lock, check the condition in a loop, wait while it's false, act, notify. here's a bounded buffer (a fixed-size queue where producers block when it's full and consumers block when it's empty) done the old way:
import java.util.ArrayDeque;
import java.util.Deque;
class BoundedBuffer<T> {
private final Deque<T> items = new ArrayDeque<>();
private final int capacity;
BoundedBuffer(int capacity) { this.capacity = capacity; }
synchronized void put(T item) throws InterruptedException {
while (items.size() == capacity) {
wait(); // full: park in the wait set, release the lock
}
items.addLast(item);
notifyAll(); // wake everyone, they'll re-check
}
synchronized T take() throws InterruptedException {
while (items.isEmpty()) {
wait(); // empty: park, release the lock
}
T item = items.removeFirst();
notifyAll();
return item;
}
}
void main() throws InterruptedException {
var buffer = new BoundedBuffer<Integer>(2);
Thread producer = Thread.ofPlatform().start(() -> {
try {
for (int i = 1; i <= 100; i++) buffer.put(i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
long sum = 0;
for (int i = 0; i < 100; i++) sum += buffer.take();
producer.join();
IO.println("sum = " + sum);
}sum = 5050
a buffer of two slots, 100 items through it, every one accounted for. now break it. two producers, two consumers, and you decide who runs:
synchronized void put(T x) { while (full) wait(); items.add(x); notifyAll(); } synchronized T take() { while (empty) wait(); T x = items.removeFirst(); notifyAll(); return x; }
empty buffer, nobody waiting. producers call put(), consumers call take().
while + notifyAll you can't break it. now switch to if, then: C1 take, C2 take, P1 put, then wake both consumers. or keep while, switch to notify, set capacity 1, and try: C1 take, C2 take, P1 put, P2 put, P1 put, C1 re-check, C2 re-check, C1 take. everyone ends up waiting.why while, not if
if checks the condition once, on the way in. after wait() returns, it just carries on, assuming the thing it waited for is still true. three reasons it might not be:
- someone else got there first. a woken thread competes for the monitor with every other thread, including brand new arrivals that never waited at all.
- notifyAll wakes everyone. five consumers woken for one item means four of them must go back to sleep.
- spurious wakeups. the spec explicitly allows
wait()to return with no notify at all (some OS-level wait primitives behave that way). rare, real, and allowed.
an empty buffer, and take() written with if instead of while.
with a while, every one of those is harmless: the thread re-checks, sees the condition is false, and waits again. with an if, every one of those is a bug that shows up once a month in prod.
void main() throws InterruptedException {
Object lock = new Object();
lock.wait();
}notify vs notifyAll
notify() wakes one waiter, chosen arbitrarily. that's only safe when every waiter is waiting for the same thing and any one of them can handle it. in the buffer, producers and consumers share one wait set but wait for opposite conditions. so notify() can wake the wrong kind of thread: a consumer's notify wakes another consumer, which finds the buffer empty and goes back to sleep, and the producer that should have woken never does. keep going and everyone ends up in the wait set, which is exactly the hang in the figure above. no exception, no CPU, just a stuck process.
notifyAll() wakes all of them. most go straight back to sleep (wasteful, but correct). use notify() only as an optimization, when you can prove one waiter is enough and it's always the right kind.
use something else
wait/notify are the assembly language of coordination. you now know how they work, which is useful for reading old code and for interviews. for new code, java.util.concurrent already has the thing you were about to build:
synchronized void put(T item) throws InterruptedException {
while (items.size() == capacity) wait();
items.addLast(item);
notifyAll();
}BlockingQueue<T> items = new ArrayBlockingQueue<>(2);
items.put(item); // blocks while full
T next = items.take(); // blocks while empty| you want to | use instead of wait/notify |
|---|---|
| hand items between threads | BlockingQueue (concurrent collections) |
| wait until N things are done | CountDownLatch (synchronizers) |
| limit how many threads do something | Semaphore |
| wait for a result | Future, CompletableFuture (CompletableFuture) |
| a condition with your own lock | Lock + Condition (locks and AQS) |
wait()releases the monitor and parks in the wait set.notify()moves a waiter to the entry set.- a woken thread still has to win the monitor back. by then the condition may be false again.
- always
while (!condition) wait();. neverif. - one monitor, one wait set.
notify()can wake the wrong kind of waiter, so prefernotifyAll(). - you must own the monitor to call any of them, or you get
IllegalMonitorStateException. - for new code:
BlockingQueue, latches, semaphores, futures. not this.