☕ Java

notifyAll()

notifyAll() wakes all threads currently in the object's wait set — every thread that has called wait() on that object and has not yet been woken. Like notify(), it must be called from within a synchronized block or method on the same object, and it throws IllegalMonitorStateException otherwise. Unlike notify(), which wakes an arbitrary single thread, notifyAll() wakes every waiter, but only one of them can reacquire the monitor at a time — they compete like any threads competing for a lock. Each woken thread re-checks its while condition; threads whose condition is still false return to the wait set. This all-wake-then-recheck behavior is the safe default when multiple conditions share a wait set, when multiple threads wait for heterogeneous signals, or when you cannot guarantee that notify() will wake a thread that can actually make progress. This entry covers the full notifyAll() contract, when it is the correct choice over notify(), the thundering herd problem and its performance cost, and when to upgrade from notifyAll() to Condition.signalAll().

notifyAll() Contract — All Waiters Wake and Compete

notifyAll() moves every thread in the object's wait set from WAITING to BLOCKED. All those threads then compete to reacquire the object's monitor. Because a monitor is a mutual exclusion mechanism, only one thread can hold it at a time — so even though all threads are woken simultaneously, they execute sequentially, each taking a turn to reacquire the monitor, check its while condition, proceed if the condition is true, and return to the wait set if it is not. This sequential execution after notifyAll() is the mechanism by which correctness is maintained. Even if N threads are woken and only one can proceed, the others loop back to wait() automatically — the while loop ensures this. The net effect is that exactly the right threads proceed and the wrong ones go back to sleep, regardless of how many were woken. The cost is that N threads are needlessly woken, context-switched, and return to sleep, when only one needed to be woken. This extra work is the thundering herd effect. notifyAll() is always safe — it cannot cause a thread to miss a notification, and it cannot cause a thread to proceed when it should not (the while loop prevents that). notify() is safe only in restricted conditions. The default recommendation for code where notify() vs notifyAll() is not a performance-critical concern is notifyAll() for safety, with upgrade to notify() only when its safety conditions are known to hold and the thundering herd cost is measurable. notifyAll() on an empty wait set, like notify() on an empty wait set, is silently ignored. notifyAll() does not queue notifications for future waiters. If a thread calls wait() after notifyAll() has already been called (and the wait set was empty at that point), that thread sleeps until the next notify/notifyAll.
Java
// ── notifyAll() wakes all — each re-checks its condition ─────────────
public class EventBus {
    private String event = null;

    public synchronized void publish(String newEvent) {
        event = newEvent;
        notifyAll();   // ALL subscribers wake and read the event
    }

    public synchronized String waitForEvent() throws InterruptedException {
        while (event == null) wait();
        return event;   // all threads that woke up return the same event
    }
}

EventBus bus = new EventBus();
// Start 5 subscriber threads:
for (int i = 0; i < 5; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            String e = bus.waitForEvent();
            System.out.println("Subscriber " + id + " received: " + e);
        } catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
    }).start();
}

Thread.sleep(100);
bus.publish("MARKET_OPEN");
// All 5 subscribers wake, check event != null, proceed
// Output (order varies): each subscriber receives MARKET_OPEN

// ── notifyAll() safe with multiple conditions ─────────────────────────
// When producers and consumers share one wait set, notifyAll() is safe:
public class SharedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public SharedBuffer(int capacity) { this.capacity = capacity; }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) wait();   // producers wait here
        queue.add(item);
        notifyAll();   // safe: producers re-check (full? wait); consumers proceed
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) wait();   // consumers wait here
        T item = queue.poll();
        notifyAll();   // safe: consumers re-check (empty? wait); producers proceed
        return item;
    }
}

// ── Thundering herd: 100 threads woken, 1 can proceed ─────────────────
public class ThunderingHerdDemo {
    private int tickets = 0;

    public synchronized void addTicket() {
        tickets++;
        notifyAll();   // ALL 100 waiting threads wake up — 99 find no ticket and re-sleep
    }

    public synchronized int takeTicket() throws InterruptedException {
        while (tickets == 0) wait();   // 99 threads return here after notifyAll()
        return tickets--;
    }
}

// 100 threads waiting:
ThunderingHerdDemo demo = new ThunderingHerdDemo();
for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        try { demo.takeTicket(); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }).start();
}
// One ticket added:
demo.addTicket();
// 100 threads wake, compete for lock, 99 re-sleep — 100 context switches for 1 actual action

// ── notifyAll() on empty wait set — no-op ────────────────────────────
Object obj = new Object();
synchronized (obj) {
    obj.notifyAll();   // silently ignored — nobody is waiting
    System.out.println("notifyAll on empty wait set: no effect");
}

// ── Comparing behavior: notify vs notifyAll ───────────────────────────
//
//                     notify()              notifyAll()
// Threads woken:      exactly 1             all in wait set
// Choice of thread:   arbitrary (JVM)       all, then compete
// Safe when:          one condition, or      always
//                     all waiters identical
// Risk:               missed wakeup if       thundering herd (performance)
//                     wrong thread woken
// Default choice:     no                    yes — safer
// Upgrade path:       use Condition.signal() use Condition.signalAll()

notifyAll() Performance, Thundering Herd, and Condition.signalAll()

The thundering herd problem is the primary performance cost of notifyAll(). When many threads are waiting and only one can proceed, notifyAll() causes all threads to wake, context-switch, attempt to acquire the monitor, check their condition, and most of them return to the wait set. The number of context switches and lock-acquisition attempts scales with the number of waiting threads, even though only one thread does actual work. In high-throughput scenarios with many threads, this can significantly degrade performance. The ideal alternative is to use precise signaling: wake only threads that can actually make progress. This requires multiple condition variables on the same lock, which is not possible with the built-in Object monitor mechanism. java.util.concurrent.locks.Condition provides this: a single ReentrantLock can create multiple Condition objects via lock.newCondition(), each with its own independent wait set. Producers wait on the notFull condition; consumers wait on the notEmpty condition. When a consumer takes an item, it calls notFull.signal() to wake exactly one producer; when a producer adds an item, it calls notEmpty.signal() to wake exactly one consumer. No false wakeups, no thundering herd. Condition.signalAll() is the Condition analog of notifyAll() — it wakes all threads waiting on that specific Condition, not all threads waiting on the lock. This is already more precise than Object.notifyAll() when multiple conditions share a lock, because it wakes only threads waiting for one condition rather than all threads waiting on the object. In practice, for production concurrent code, the decision tree is: use BlockingQueue for producer-consumer (it handles all of this internally); use Condition with precise signal() for custom concurrent data structures where thundering herd is a measured concern; use notifyAll() for correctness in simpler cases where performance is not a bottleneck. The JVM also applies various optimizations to reduce thundering herd overhead (e.g., biased locking, adaptive spinning), so measured performance should guide optimization decisions rather than theoretical analysis alone.
Java
// ── Condition.signalAll() — precise, per-condition wakeup ────────────
import java.util.concurrent.locks.*;

public class HighThroughputBuffer<T> {
    private final Object[] data;
    private int head, tail, count;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    @SuppressWarnings("unchecked")
    public HighThroughputBuffer(int capacity) {
        data = new Object[capacity];
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (count == data.length) notFull.await();   // only producers here
            data[tail] = item;
            tail = (tail + 1) % data.length;
            count++;
            notEmpty.signal();   // wake exactly ONE consumer — no herd
        } finally { lock.unlock(); }
    }

    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) notEmpty.await();   // only consumers here
            T item = (T) data[head];
            data[head] = null;
            head = (head + 1) % data.length;
            count--;
            notFull.signal();   // wake exactly ONE producer — no herd
            return item;
        } finally { lock.unlock(); }
    }
}

// ── Performance comparison: notifyAll vs Condition.signal ─────────────
// Scenario: 50 producers, 50 consumers, buffer capacity 10

// With Object.notifyAll():
//   Each put() call: 50 threads wake (50 wasted context switches per operation)
//   Each take() call: 50 threads wake (50 wasted context switches per operation)
//   Total overhead per item: ~100 unnecessary context switches

// With Condition.signal():
//   Each put() call: 1 consumer wakes (0 wasted context switches)
//   Each take() call: 1 producer wakes (0 wasted context switches)
//   Total overhead per item: 0 unnecessary context switches

// For 1,000,000 items:
//   notifyAll(): ~100,000,000 unnecessary context switches (~100x overhead)
//   signal():    ~0 unnecessary context switches

// ── signalAll() for broadcast scenarios ──────────────────────────────
// Even with Condition, signalAll() is sometimes the right choice:
public class StartingGun {
    private boolean fired = false;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition ready    = lock.newCondition();

    public void fire() {
        lock.lock();
        try {
            fired = true;
            ready.signalAll();   // ALL waiting threads should start — broadcast correct here
        } finally { lock.unlock(); }
    }

    public void awaitStart() throws InterruptedException {
        lock.lock();
        try {
            while (!fired) ready.await();
        } finally { lock.unlock(); }
    }
}

StartingGun gun = new StartingGun();
for (int i = 0; i < 10; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            gun.awaitStart();
            System.out.println("Runner " + id + " started!");
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }).start();
}
Thread.sleep(500);
gun.fire();  // all 10 runners start simultaneously — signalAll() is correct here

// ── Decision guide: notify vs notifyAll vs Condition ─────────────────
// 1. Only one condition, interchangeable waiters:
//    → notify() is safe and efficient

// 2. Multiple conditions on same lock, or heterogeneous waiters:
//    → notifyAll() for correctness; accept thundering herd

// 3. Multiple conditions, high throughput required:
//    → Condition.signal() with separate notFull/notEmpty conditions

// 4. Broadcast to all waiters (startup, event publication):
//    → notifyAll() or Condition.signalAll() — both are correct here

// 5. Complex concurrent data structure in production:
//    → Use java.util.concurrent (BlockingQueue, etc.) — pre-optimized

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.