skip to content
everything.concurrency.waitAndNotify

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.

outsideRUNNABLE
T1T2T3
entry setBLOCKED
monitor of lockowner
free
wait setWAITING
T1
T2
T3

nobody owns the monitor. any thread that enters synchronized (lock) gets it straight away.

enter with T1 and call wait(): T1 lands in the wait set and the monitor is free. enter with T2 and call notify(): T1 moves to the entry set, not straight back inside. it only returns from wait() when T2 exits and T1 wins the monitor back, with its old hold count restored.

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:

Main.java
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);
}
output
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:

loop =
wake =
capacity =
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;
}
buffer0/2
··
wait set
empty
P1
P2
C1
C2

empty buffer, nobody waiting. producers call put(), consumers call take().

with 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:

  1. 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.
  2. notifyAll wakes everyone. five consumers woken for one item means four of them must go back to sleep.
  3. 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.
C1
C2
P
shareditems
·
[]

an empty buffer, and take() written with if instead of while.

step 1/6
the first reason, step by step. the woken thread isn't first in line for anything, and the item it was woken for is gone by the time it runs.

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.

predict the output
java
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:

don'thand-rolled guarded blocks
java
synchronized void put(T item) throws InterruptedException {
    while (items.size() == capacity) wait();
    items.addLast(item);
    notifyAll();
}
doa BlockingQueue
java
BlockingQueue<T> items = new ArrayBlockingQueue<>(2);
 
items.put(item);        // blocks while full
T next = items.take();  // blocks while empty
you want touse instead of wait/notify
hand items between threadsBlockingQueue (concurrent collections)
wait until N things are doneCountDownLatch (synchronizers)
limit how many threads do somethingSemaphore
wait for a resultFuture, CompletableFuture (CompletableFuture)
a condition with your own lockLock + Condition (locks and AQS)
keylines
  • 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();. never if.
  • one monitor, one wait set. notify() can wake the wrong kind of waiter, so prefer notifyAll().
  • you must own the monitor to call any of them, or you get IllegalMonitorStateException.
  • for new code: BlockingQueue, latches, semaphores, futures. not this.