☕ Java

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() removes exactly one thread from the object's wait set and transitions it from WAITING to BLOCKED (waiting to reacquire the monitor). If the wait set is empty, notify() has no effect and is silently ignored — the notification is not queued or stored. If multiple threads are in the wait set, the JVM chooses one arbitrarily; the choice is implementation-defined and should never be relied upon. The notifying thread retains the monitor after calling notify(). The woken thread does not immediately execute — it enters the BLOCKED state and must compete for the monitor like any other thread. Only when the notifier (and any other synchronized methods that complete) releases the monitor does the woken thread get a chance to acquire it and return from wait(). This means the notifier can perform additional work after notify() before releasing the lock. notify() must always be called after the shared state has been updated. The contract of wait/notify is: (1) producer updates shared state; (2) producer calls notify(); (3) consumer wakes from wait(), reacquires monitor, and observes the updated state. If notify() is called before the state is updated — or worse, without any state update at all — the woken consumer re-checks its while condition, finds it still false, and goes back to wait(). This is benign but wasteful. The critical requirement: notify() must be called inside a synchronized block on the same object. This is not just a Java rule but a logical necessity. If the notifier is not synchronized, it may call notify() while the waiter is between checking the condition and calling wait() — the classic check-then-act race. Synchronization ensures the notifier holds the monitor when it signals, meaning the waiter cannot be in mid-transition. Once the notifier holds the lock, the waiter is either in the wait set (and will receive the signal) or has not yet entered (and will check the updated condition before deciding to wait).
Java
// ── 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() is safe when two conditions hold simultaneously: all threads in the wait set are waiting for the same condition, and waking any one of them will make progress. When both conditions hold, waking an arbitrary thread is correct — whichever thread wakes will be able to proceed. If either condition fails, notify() may cause a thread that cannot make progress to be woken (wasting a wakeup), while a thread that could make progress remains sleeping, causing the program to hang. The conditions fail in two common scenarios. First: multiple distinct conditions sharing one wait set. If producers wait for "not full" and consumers wait for "not empty" on the same object, a notify() by a producer may wake another producer (which still finds the buffer full) while a consumer that could have taken the item remains sleeping. The fix is either notifyAll() or (better) using java.util.concurrent.locks.Condition to have separate wait sets for the two conditions. Second: multiple threads waiting for the same condition but only one can proceed. If three consumers are waiting for an item and one item is produced, only one consumer can take it. notify() is safe here — whichever consumer wakes will take the item. But if after taking the item the consumer produces another item and calls notify(), the woken thread must be one that is still waiting. If the woken thread was already woken and is trying to reacquire the monitor, the notify() goes to a thread in the wait set. The while-loop guarantees that any thread that cannot actually proceed goes back to wait(), so notify() is correct here. The safe usage rule: use notify() when (a) there is only one condition variable being waited on, or (b) all waiters for that condition are interchangeable and each wakeup will be used. Use notifyAll() when there are multiple conditions sharing a wait set, when threads wait for heterogeneous conditions, or when correctness requires that all waiters re-check rather than one.
Java
// ── 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(); }
    }
}

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.