wait()
wait() is an instance method on Object that causes the current thread to release the object's monitor lock and suspend until another thread calls notify() or notifyAll() on the same object, or until a specified timeout elapses. It is the blocking half of Java's built-in inter-thread communication system. wait() must always be called from within a synchronized block or method on the same object — attempting to call it outside a synchronized context throws IllegalMonitorStateException. The call to wait() is atomic with respect to the lock release: the thread releases the monitor and enters the wait set in one indivisible operation, preventing the race condition where a notify() could be missed between the lock release and the suspension. This entry covers all three overloads of wait(), the mandatory while-loop pattern for spurious wakeups, the wait set model, what happens to thread state during wait(), the interrupted status and InterruptedException contract, timed waits and their return semantics, and the strict ownership requirement.
wait() Mechanics — Lock Release, Wait Set, and Wakeup
// ── The three overloads of wait() ─────────────────────────────────────
synchronized (lock) {
lock.wait(); // wait indefinitely — returns only on notify/notifyAll/interrupt
lock.wait(5000); // wait at most 5000 milliseconds
lock.wait(5000, 500_000); // wait at most 5000ms + 500,000ns = 5000.5ms
}
// ── IllegalMonitorStateException if not in synchronized context ────────
Object obj = new Object();
try {
obj.wait(); // THROWS IllegalMonitorStateException — not synchronized on obj
} catch (IllegalMonitorStateException e) {
System.out.println("Must be synchronized on obj before calling obj.wait()");
}
// ── Atomic lock-release-and-wait prevents missed notify ───────────────
// The classic missed-signal problem wait() is designed to prevent:
// Producer:
synchronized (lock) {
condition = true; // set condition
lock.notify(); // signal waiter
}
// Consumer (wait() is atomic with respect to the above):
synchronized (lock) {
// CANNOT miss the notify: if producer already ran, condition is true
// and we skip the wait. If producer runs DURING our wait(), it wakes us.
// There is no window between the check and the wait where notify() is lost.
while (!condition) {
lock.wait(); // atomic: release lock + enter wait set
}
// condition is guaranteed true here
}
// ── Thread state transitions ───────────────────────────────────────────
//
// notify()/notifyAll()/timeout/interrupt
// RUNNABLE ┌──────────────────────────────────┐
// │ ▼ │
// │ wait() WAITING / TIMED_WAITING must reacquire monitor
// └──────────────► (in wait set, no CPU, ──────────────► BLOCKED ──────► RUNNABLE
// no locks held) (waiting for lock)
// ── Demonstrating the wait set and reacquisition ──────────────────────
Object gate = new Object();
boolean[] ready = {false};
Thread waiter = new Thread(() -> {
synchronized (gate) {
System.out.println("Waiter: checking condition");
while (!ready[0]) {
try {
System.out.println("Waiter: condition false, calling wait()");
gate.wait(); // releases gate's monitor, enters wait set
System.out.println("Waiter: woken up, reacquired monitor, rechecking");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("Waiter: condition true, proceeding");
}
});
Thread notifier = new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) {}
synchronized (gate) {
System.out.println("Notifier: setting condition and calling notify()");
ready[0] = true;
gate.notify(); // wakes waiter — waiter then competes for the monitor
System.out.println("Notifier: released monitor");
} // notifier releases monitor here — waiter can now reacquire it
});
waiter.start(); notifier.start();
waiter.join(); notifier.join();
// Waiter: checking condition
// Waiter: condition false, calling wait()
// Notifier: setting condition and calling notify()
// Notifier: released monitor
// Waiter: woken up, reacquired monitor, rechecking
// Waiter: condition true, proceedingSpurious Wakeups and the Mandatory while-Loop Pattern
// ── ALWAYS while, NEVER if ───────────────────────────────────────────
// WRONG — if-based wait: vulnerable to spurious wakeups AND notifyAll():
synchronized (lock) {
if (queue.isEmpty()) { // BAD: if
lock.wait(); // spurious wakeup returns here with queue still empty
}
Object item = queue.remove(); // NullPointerException or NoSuchElementException!
}
// CORRECT — while-loop: safe against all wakeup scenarios:
synchronized (lock) {
while (queue.isEmpty()) { // GOOD: while
lock.wait(); // spurious wakeup loops back to check condition again
}
Object item = queue.remove(); // guaranteed: queue is non-empty
}
// ── notifyAll wakes multiple waiters — while loop handles extra wakeups ─
public class SingleItemBox<T> {
private T item = null;
public synchronized void put(T value) throws InterruptedException {
while (item != null) { // wait while full
wait();
}
item = value;
notifyAll(); // wake ALL — including other potential producers
}
public synchronized T take() throws InterruptedException {
while (item == null) { // wait while empty
wait(); // spurious or extra wakeup: loop re-checks
}
T result = item;
item = null;
notifyAll(); // wake ALL — including other potential consumers
return result;
}
}
// Scenario: 3 consumers waiting, 1 item produced:
// notifyAll() wakes all 3 consumers
// Consumer 1 reacquires lock, takes item, sets item = null, notifyAll()
// Consumer 2 reacquires lock, while (item == null) → true → waits again
// Consumer 3 reacquires lock, while (item == null) → true → waits again
// Without the while loop, consumers 2 and 3 would try to take a null item
// ── InterruptedException contract ─────────────────────────────────────
// Option 1: propagate the exception — caller handles interruption:
public synchronized void waitForCondition() throws InterruptedException {
while (!conditionMet()) {
wait(); // throws InterruptedException if thread is interrupted
}
}
// Option 2: restore interrupt status — for Runnable implementations:
public synchronized void waitSilently() {
while (!conditionMet()) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the interrupt flag
return; // exit the wait loop
}
}
}
// Option 3: treat interruption as cancellation — check a flag:
private volatile boolean cancelled = false;
public synchronized void waitCancellable() throws InterruptedException {
while (!conditionMet() && !cancelled) {
try {
wait(100); // timed wait — periodically re-check cancelled
} catch (InterruptedException e) {
cancelled = true;
Thread.currentThread().interrupt();
throw e;
}
}
}
// ── Timed wait — cannot distinguish timeout from notify ───────────────
synchronized (lock) {
long deadline = System.currentTimeMillis() + 5000;
while (!condition) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
System.out.println("Timed out waiting for condition");
break; // or throw TimeoutException
}
lock.wait(remaining); // wait the remaining time — re-checks on each wakeup
}
if (condition) {
System.out.println("Condition met within timeout");
}
}