☕ Java

CountDownLatch

CountDownLatch is a synchronization aid that allows one or more threads to wait until a set of operations being performed by other threads completes. It is initialized with a count; threads waiting call await(), which blocks until the count reaches zero; other threads call countDown() to decrement the count. When the count reaches zero, all waiting threads are released simultaneously and all subsequent calls to await() return immediately. CountDownLatch is one-shot — once the count reaches zero, it cannot be reset. For repeatable barriers, use CyclicBarrier or Phaser. CountDownLatch covers two fundamental patterns: waiting for N events (one await thread, N countDown callers), and a starting gun (N await threads, one countDown caller). Its simplicity, correctness guarantees, and performance make it the standard tool for coordinating startup sequences, parallel task completion, and test synchronization in concurrent Java code. This entry covers the full API, the happens-before guarantee, the two canonical patterns and their variants, comparison with CyclicBarrier and Phaser, and implementation details.

API, Happens-Before Guarantee, and One-Shot Semantics

CountDownLatch is constructed with new CountDownLatch(int count), establishing the initial count. countDown() decrements the count by one; if the count reaches zero, all threads blocked in await() are released. countDown() never blocks and has no effect if the count is already zero. getCount() returns the current count — a best-effort snapshot useful for monitoring and debugging, not for making synchronization decisions. await() blocks the calling thread until the count reaches zero. If the count is already zero when await() is called, it returns immediately without blocking. await(long timeout, TimeUnit unit) waits up to the specified duration and returns false if the count did not reach zero within the timeout, true if it did. Both forms respond to interruption: if the waiting thread is interrupted, InterruptedException is thrown. The happens-before guarantee: all actions performed by a thread before calling countDown() happen-before the return of await() in a waiting thread. This is a critical guarantee — it means that the work completed by the countdown threads (writing results, updating data structures) is fully visible to threads that wake up from await(). Without this guarantee, a thread that wakes from await() and reads results written by countdown threads could see stale or incomplete data. The one-shot property: CountDownLatch's count can never increase. Once it reaches zero, it stays at zero. No mechanism exists to reset or reuse it. This is intentional — reusable barriers with reset capability are provided by CyclicBarrier (fixed party count) and Phaser (dynamic party registration). When a one-time coordination event is needed — system startup, test preparation, parallel batch completion — CountDownLatch is the right choice precisely because its one-shot nature provides a simple, unambiguous model.
Java
// ── Basic CountDownLatch ──────────────────────────────────────────────
CountDownLatch latch = new CountDownLatch(3);  // count starts at 3

// Three worker threads each call countDown() when done:
for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            System.out.println("Worker " + id + ": working");
            Thread.sleep(100 * (id + 1));   // simulate different durations
            System.out.println("Worker " + id + ": done");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown();   // decrement — does NOT block
        }
    }).start();
}

// Main thread waits until all 3 workers finish:
latch.await();
System.out.println("All workers done — proceeding");

// ── getCount: monitoring, not synchronization ─────────────────────────
CountDownLatch monitor = new CountDownLatch(5);
System.out.println("Initial count: " + monitor.getCount());   // 5
monitor.countDown();
System.out.println("After one countdown: " + monitor.getCount());  // 4
// getCount() is best-effort — don't use for synchronization decisions

// ── Timed await ───────────────────────────────────────────────────────
CountDownLatch timedLatch = new CountDownLatch(1);
new Thread(() -> {
    try { Thread.sleep(300); } catch (InterruptedException e) {}
    timedLatch.countDown();
}).start();

try {
    if (timedLatch.await(500, TimeUnit.MILLISECONDS)) {
        System.out.println("Completed within 500ms");
    } else {
        System.out.println("Did not complete within 500ms");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// ── Happens-before: countdown thread writes are visible to awaiting thread ─
String[] result = {null};
CountDownLatch done = new CountDownLatch(1);

new Thread(() -> {
    result[0] = "computed value";   // write happens before countDown()
    done.countDown();               // happens-before done.await() returning
}).start();

done.await();
System.out.println(result[0]);   // guaranteed to see "computed value" — not null
// Without happens-before, result[0] might still appear null to main thread

// ── One-shot: count cannot increase or reset ──────────────────────────
CountDownLatch oneShot = new CountDownLatch(2);
oneShot.countDown();   // count: 1
oneShot.countDown();   // count: 0 — latch OPENS
oneShot.await();       // returns immediately — already at 0
oneShot.countDown();   // no-op — count stays at 0
oneShot.await();       // returns immediately — still at 0

The Two Canonical Patterns

The wait-for-N-tasks pattern has one or more master threads waiting and N worker threads each calling countDown() once when done. This is the most common use: a coordinator submits N parallel tasks and then waits for all to complete before proceeding. Each task calls countDown() in a finally block to guarantee the latch is decremented even if the task fails. The pattern composes naturally with ExecutorService: submit N tasks to a thread pool, each calling countDown() in its finally block, then call await() on the main thread. This avoids creating N dedicated threads and handles thread pool queue capacity correctly. The equivalent using Future.get() in a loop would require sequential waiting (get() on future 1, then get() on future 2, etc.) unless care is taken to use invokeAll() or to collect all futures before waiting. The starting gun (or ready-aim-fire) pattern has one latch (or zero permits) controlling when N threads are allowed to start. All N threads call await() and block; one control thread calls countDown() (usually after doing setup work); all N threads are released simultaneously. This is useful in benchmarks (all threads start at exactly the same moment to maximize contention), in tests (all threads start after the system under test is fully initialized), or in coordinated parallel algorithms (all workers start their phase simultaneously). Combining both patterns is common: one starting gun latch (count=1) to synchronize the start, and one completion latch (count=N) to wait for all workers to finish. This ensures that all workers start simultaneously (maximizing parallelism) and the coordinator waits for all to complete before collecting results or proceeding to the next phase.
Java
// ── Pattern 1: wait-for-N-tasks ──────────────────────────────────────
public static <T> List<T> parallelCompute(
        List<Callable<T>> tasks, ExecutorService executor)
        throws InterruptedException {

    int n = tasks.size();
    CountDownLatch done    = new CountDownLatch(n);
    List<T>        results = new CopyOnWriteArrayList<>();
    List<Throwable> errors = new CopyOnWriteArrayList<>();

    for (Callable<T> task : tasks) {
        executor.submit(() -> {
            try {
                results.add(task.call());
            } catch (Throwable t) {
                errors.add(t);
            } finally {
                done.countDown();   // ALWAYS decrement, even on failure
            }
        });
    }

    done.await();   // wait until ALL tasks complete (success or failure)

    if (!errors.isEmpty()) {
        throw new RuntimeException("Tasks failed: " + errors.size() + " errors", errors.get(0));
    }
    return results;
}

// Usage:
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Callable<Integer>> tasks = List.of(
    () -> { Thread.sleep(100); return 1; },
    () -> { Thread.sleep(200); return 2; },
    () -> { Thread.sleep(150); return 3; }
);
List<Integer> results = parallelCompute(tasks, pool);
System.out.println(results);   // [1, 2, 3] in some order — all completed

// ── Pattern 2: starting gun ───────────────────────────────────────────
public static void benchmarkWithStartingGun(int threads, Runnable task)
        throws InterruptedException {

    CountDownLatch startGun  = new CountDownLatch(1);
    CountDownLatch allDone   = new CountDownLatch(threads);
    long[]         times     = new long[threads];

    for (int i = 0; i < threads; i++) {
        final int id = i;
        new Thread(() -> {
            try {
                startGun.await();       // all threads park here waiting for the gun
                long start = System.nanoTime();
                task.run();
                times[id] = System.nanoTime() - start;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                allDone.countDown();
            }
        }).start();
    }

    Thread.sleep(100);  // let all threads reach startGun.await()
    startGun.countDown();  // FIRE — all threads start simultaneously
    allDone.await();       // wait for all to finish

    LongSummaryStatistics stats = Arrays.stream(times).summaryStatistics();
    System.out.printf("Min: %.2fms  Max: %.2fms  Avg: %.2fms%n",
        stats.getMin()     / 1e6,
        stats.getMax()     / 1e6,
        stats.getAverage() / 1e6);
}

// ── Combined pattern: start gun + completion latch ────────────────────
public class ParallelPhase {
    public static void run(int workers, Runnable[] work)
            throws InterruptedException {

        CountDownLatch ready      = new CountDownLatch(workers);  // workers signal ready
        CountDownLatch startGun   = new CountDownLatch(1);        // coordinator fires gun
        CountDownLatch completed  = new CountDownLatch(workers);  // workers signal done

        for (int i = 0; i < workers; i++) {
            final int idx = i;
            new Thread(() -> {
                ready.countDown();        // signal: I'm ready
                try {
                    startGun.await();     // wait for coordinator's go-ahead
                    work[idx].run();      // do assigned work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    completed.countDown();
                }
            }).start();
        }

        ready.await();       // wait until all workers are ready
        System.out.println("All workers ready — firing start gun");
        startGun.countDown(); // release all workers simultaneously
        completed.await();   // wait until all finish
        System.out.println("All workers completed");
    }
}

CountDownLatch vs CyclicBarrier vs Phaser

CountDownLatch, CyclicBarrier, and Phaser all coordinate threads waiting for each other, but they differ in resetability, symmetry, and flexibility. Choosing the right one depends on whether the barrier is one-shot or reusable, whether the coordinating party is the same as the waiting parties, and whether the party count changes over time. CountDownLatch is one-shot, asymmetric (the thread calling countDown() need not be the same as the thread calling await()), and the count is fixed at construction. Use it for: waiting for a fixed set of tasks to complete, starting gun for N threads, waiting for system initialization (N services start up, coordinator waits for all). It cannot be reused for a second round without creating a new instance. CyclicBarrier is reusable, symmetric (all parties call await() and all must arrive before any proceeds), and has a fixed party count. Use it for: iterative parallel algorithms where N threads must synchronize between phases, multi-player game turn coordination, parallel data processing with between-step aggregation. An optional barrier action runs once when all parties arrive, in the last-arriving thread's context. If a thread times out or is interrupted, all parties receive BrokenBarrierException, and the barrier is broken and unusable without reset(). Phaser is the most flexible: party count is dynamic (register/deregister at any time), it is reusable across multiple phases, arrivals and waiting can be separated, and threads can arrive without waiting (arriveAndDeregister()) or wait for a specific phase. Use it for: algorithms with variable work per phase, fork-join style computation with parent-child relationships (parent phaser with child phasers), streaming pipelines where workers come and go. Phaser subsumes both CountDownLatch (phase with one waiting thread) and CyclicBarrier (symmetric arrival), but its API is more complex.
Java
// ── CyclicBarrier: reusable, symmetric ────────────────────────────────
int PARTIES = 3;
CyclicBarrier barrier = new CyclicBarrier(PARTIES, () ->
    System.out.println("=== All parties arrived at barrier — entering next phase ===")
);

for (int round = 0; round < 3; round++) {   // 3 phases — barrier is reused
    for (int i = 0; i < PARTIES; i++) {
        final int id = i;
        new Thread(() -> {
            try {
                System.out.println("Thread " + id + ": working on phase");
                Thread.sleep((long)(Math.random() * 300));
                System.out.println("Thread " + id + ": arrived at barrier");
                barrier.await();             // waits for all PARTIES to arrive
                System.out.println("Thread " + id + ": proceeding to next phase");
            } catch (BrokenBarrierException | InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
    Thread.sleep(1000);  // let round complete before next
}
// barrier automatically resets after each phase — no new CyclicBarrier needed

// ── Phaser: dynamic parties and phased computation ────────────────────
Phaser phaser = new Phaser(1);  // register main thread as party

int WORKERS = 4;
for (int i = 0; i < WORKERS; i++) {
    phaser.register();          // each worker registers dynamically
    final int id = i;
    new Thread(() -> {
        try {
            System.out.println("Worker " + id + ": phase 1 work");
            Thread.sleep((long)(Math.random() * 200));
            phaser.arriveAndAwaitAdvance();   // wait at phase 1 barrier

            System.out.println("Worker " + id + ": phase 2 work");
            Thread.sleep((long)(Math.random() * 200));
            phaser.arriveAndDeregister();     // done — deregister (don't wait for others)
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }).start();
}

phaser.arriveAndAwaitAdvance();  // main thread waits at phase 1
System.out.println("All workers completed phase 1");
phaser.arriveAndDeregister();    // main thread deregisters

// ── Comparison summary ────────────────────────────────────────────────
//
//                  CountDownLatch  CyclicBarrier   Phaser
// ─────────────────────────────────────────────────────────────────────
// Resetable?       No              Yes (reset())   Yes (built-in)
// Symmetric?       No (N:1 or 1:N) Yes (N:N)       Configurable
// Dynamic parties? No              No              Yes (register/deregister)
// Multiple phases? No (one-shot)   Yes (auto-reset)Yes (unlimited phases)
// Barrier action?  No              Yes             No (override onAdvance)
// Interruption?    Propagates      BrokenBarrier   Propagates
// Best for         Task completion Iterative alg.  Complex pipelines
//                  Starting guns   Phase sync      Dynamic fork-join

// ── Choosing: decision tree ───────────────────────────────────────────
// Q: Is this coordination one-time only?
//    Yes → CountDownLatch
//    No  → Q: Is the party count fixed and equal for all phases?
//            Yes → CyclicBarrier
//            No  → Phaser

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.