notify()
notify() is an instance method on Object that wakes exactly one thread from the object's wait set — the set of threads currently blocked in wait() on that object. Which thread is woken is not specified by the Java specification and depends on the JVM implementation and OS scheduler. notify() must be called from within a synchronized block or method on the same object, and it throws IllegalMonitorStateException otherwise. Calling notify() does not immediately transfer control to the woken thread — the notifier continues executing and holds the monitor until it exits its synchronized block, at which point the woken thread competes to reacquire the monitor. This entry covers the notify() contract in full, when notify() is correct vs when notifyAll() is required, the missed notification problem and why notification must happen after state change, the dangers of conditional notification, and how to structure notification correctly in producer-consumer and state-change patterns.
notify() Contract — Single Wakeup and Monitor Semantics
// ── notify() selects one arbitrary thread from wait set ──────────────
public class PingPong {
private boolean pingTurn = true;
public synchronized void ping() throws InterruptedException {
while (!pingTurn) wait();
System.out.println("PING");
pingTurn = false;
notify(); // wakes the single thread waiting in pong()
}
public synchronized void pong() throws InterruptedException {
while (pingTurn) wait();
System.out.println("PONG");
pingTurn = true;
notify(); // wakes the single thread waiting in ping()
}
}
PingPong pp = new PingPong();
new Thread(() -> {
try { for (int i=0; i<5; i++) pp.ping(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();
new Thread(() -> {
try { for (int i=0; i<5; i++) pp.pong(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();
// PING PONG PING PONG PING PONG PING PONG PING PONG
// ── Notifier holds monitor after notify() — woken thread waits ────────
public class NotifierHoldsLock {
private boolean ready = false;
public synchronized void produce() throws InterruptedException {
ready = true; // 1. update state
notify(); // 2. wake one waiter
System.out.println("Notifier: continuing after notify()"); // 3. still running
Thread.sleep(200); // 4. still holds monitor
System.out.println("Notifier: releasing monitor");
} // 5. monitor released here — woken thread can now proceed
public synchronized void consume() throws InterruptedException {
while (!ready) wait();
System.out.println("Consumer: woke up and consumed");
}
}
// ── State must be updated BEFORE notify() ─────────────────────────────
public class SharedState {
private String data = null;
// CORRECT: update state, then notify
public synchronized void setData(String value) {
data = value; // 1. set data FIRST
notify(); // 2. notify AFTER — consumer will find data != null
}
// WRONG: notify before state update
public synchronized void setDataWrong(String value) {
notify(); // 1. consumer wakes, re-checks while (data == null) → true → waits again
data = value; // 2. update happens too late — notification was wasted
}
public synchronized String getData() throws InterruptedException {
while (data == null) wait();
return data;
}
}
// ── notify() on empty wait set is silently ignored ─────────────────────
Object obj = new Object();
synchronized (obj) {
obj.notify(); // wait set is empty — no-op, no exception, no effect
System.out.println("notify() on empty wait set: no effect");
}
// ── notify() requires synchronized — IllegalMonitorStateException ──────
Object lock = new Object();
try {
lock.notify(); // THROWS — current thread doesn't own lock's monitor
} catch (IllegalMonitorStateException e) {
System.out.println("Must be synchronized: " + e.getMessage());
}When notify() Is Safe vs When notifyAll() Is Required
// ── notify() safe: one condition, one waiter type ────────────────────
public class SingleConditionQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public SingleConditionQueue(int capacity) { this.capacity = capacity; }
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) wait(); // producers wait: "not full"
queue.add(item);
notify(); // safe: only consumers wait on this object, all wait for "not empty"
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) wait(); // consumers wait: "not empty"
T item = queue.poll();
notify(); // safe: only producers wait for "not full", and only one needs to wake
return item;
}
}
// ── notify() UNSAFE: two conditions, same wait set ────────────────────
public class TwoConditionOneLock<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public TwoConditionOneLock(int capacity) { this.capacity = capacity; }
// PROBLEM: producers wait for "not full", consumers wait for "not empty"
// Both are in the SAME wait set.
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) wait(); // producer waits
queue.add(item);
notify(); // DANGEROUS: might wake a producer waiting for "not full"
// while ALL consumers remain sleeping → producer sees full buffer
// → waits again → nobody processes → DEADLOCK
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) wait(); // consumer waits
T item = queue.poll();
notify(); // DANGEROUS: might wake a consumer waiting for "not empty"
// while producers waiting for "not full" remain sleeping → DEADLOCK
return item;
}
}
// ── Fix 1: use notifyAll() instead of notify() ────────────────────────
public synchronized void putSafe(T item) throws InterruptedException {
while (queue.size() == capacity) wait();
queue.add(item);
notifyAll(); // wakes EVERYONE — producers re-check (still full? wait) consumers proceed
}
public synchronized T takeSafe() throws InterruptedException {
while (queue.isEmpty()) wait();
T item = queue.poll();
notifyAll(); // wakes EVERYONE — consumers re-check (still empty? wait) producers proceed
return item;
}
// ── Fix 2 (better): separate Condition variables per condition ─────────
import java.util.concurrent.locks.*;
public class TwoConditionCorrect<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // producers wait here
private final Condition notEmpty = lock.newCondition(); // consumers wait here
public TwoConditionCorrect(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(); // wake ONE consumer — precise, no false wakeups
} finally { lock.unlock(); }
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
T item = queue.poll();
notFull.signal(); // wake ONE producer — precise
return item;
} finally { lock.unlock(); }
}
}