☕ Java

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

Object.wait() has three overloads: wait() waits indefinitely; wait(long timeoutMillis) waits at most the specified number of milliseconds; wait(long timeoutMillis, int nanos) adds nanosecond precision to the timeout (in practice the nanosecond component rounds to the nearest millisecond on most JVMs). All three must be called within a synchronized block or method on the object being waited on, and all three throw IllegalMonitorStateException if the calling thread does not own the object's monitor. The semantics of wait() are: atomically release the object's monitor and move the current thread into the object's wait set. The two operations — release the lock and enter the wait state — are atomic with respect to notify(): there is no window between them during which a notify() could be called and lost. This is the key contract that makes wait/notify correct. Without this atomicity, a producer could write a value and call notify() after the consumer checked the condition (found it false) but before the consumer called wait(), and the notify() would go to an empty wait set, causing the consumer to wait forever for a notify that already happened. While in the wait set, the thread is in the WAITING state (indefinite wait) or TIMED_WAITING state (timed wait). It does not consume CPU. It holds no locks. Other threads can freely acquire the object's monitor while this thread waits. A thread leaves the wait set and becomes eligible to run when: notify() is called on the same object (wakes one arbitrary thread from the wait set); notifyAll() is called (wakes all threads in the wait set); the timeout expires (for timed waits); or the thread is interrupted (sets the thread's interrupted status and causes wait() to throw InterruptedException). After being woken, the thread does not immediately resume — it must first reacquire the monitor (competing with all other threads that want it), and only then does wait() return.
Java
// ── 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, proceeding

Spurious Wakeups and the Mandatory while-Loop Pattern

A spurious wakeup is a wakeup that occurs without any call to notify() or notifyAll() and without timeout expiration or interruption. The Java specification explicitly permits spurious wakeups: the JVM is allowed to wake a waiting thread for no reason. This is not a theoretical concern — on Linux, spurious wakeups from pthread_cond_wait() (which the JVM uses) are documented and occur in practice, often under load or when signals are delivered to the process. The consequence is non-negotiable: every wait() call must be inside a while loop that re-checks the condition. The pattern is always while (!condition) { wait(); }, never if (!condition) { wait(); }. An if-based wait will re-enter the protected code after a spurious wakeup even though the condition is still false, corrupting the shared state. The while loop also handles another wakeup scenario: the thread is woken by notifyAll() but the condition is not true for this particular thread. notifyAll() wakes every thread in the wait set. If there are five consumers waiting for items and only one item is added, all five are woken, but only one should proceed. The four that find the condition still false loop back to wait(). Without the while loop, all four would proceed incorrectly. Additionally, the while loop handles the case where the condition becomes false again between the wakeup and the actual reacquisition of the monitor. If a thread is woken and the condition is true at the time of wakeup but another thread acquires the monitor first and makes the condition false again, the while loop catches this re-falsification. The canonical pattern for any wait/notify usage: always synchronized, always while loop, always re-check, always handle InterruptedException by restoring the interrupt status if not re-throwing.
Java
// ── 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");
    }
}

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.