Home Java CountDownLatch vs CyclicBarrier in Java — Deep Dive with Real Examples

CountDownLatch vs CyclicBarrier in Java — Deep Dive with Real Examples

In Plain English 🔥
Imagine a rocket launch. The countdown — 10, 9, 8 … 1, 0 — happens once, and when it hits zero, the rocket fires. That's a CountDownLatch: a one-shot gate that opens when a count reaches zero. Now imagine a relay race where all four runners must reach the exchange zone before anyone passes the baton. Once they're all there, they all go — and the next lap can repeat the same wait. That's a CyclicBarrier: a reusable meeting point that resets after every group finishes.
⚡ Quick Answer
Imagine a rocket launch. The countdown — 10, 9, 8 … 1, 0 — happens once, and when it hits zero, the rocket fires. That's a CountDownLatch: a one-shot gate that opens when a count reaches zero. Now imagine a relay race where all four runners must reach the exchange zone before anyone passes the baton. Once they're all there, they all go — and the next lap can repeat the same wait. That's a CyclicBarrier: a reusable meeting point that resets after every group finishes.

Modern Java applications rarely run a single task at a time. Whether you're loading config from three different microservices before serving the first request, running parallel test suites, or coordinating phases in a data-processing pipeline, you need threads to wait for each other in a controlled, predictable way. Get this wrong and you end up with race conditions, deadlocks, or — the sneaky worst case — a service that silently produces incomplete results because one thread raced ahead before the others were ready.

Both CountDownLatch and CyclicBarrier live in java.util.concurrent and solve the 'threads waiting for each other' problem, but they solve subtly different flavours of it. CountDownLatch is about one or more threads waiting until a set of operations performed by other threads completes — think dependencies. CyclicBarrier is about a fixed group of threads all waiting until every member of that group is ready to proceed together — think synchronisation points in iterative work.

By the end of this article you'll understand the internal mechanics of both primitives, know exactly which one to reach for in a given situation, be able to explain their trade-offs in an interview without hesitation, and have production-ready patterns you can drop straight into your codebase.

CountDownLatch — Internals, Lifecycle and When to Reach for It

CountDownLatch wraps an AbstractQueuedSynchronizer (AQS) state integer. When you call new CountDownLatch(n), the AQS state is initialised to n. Every countDown() call performs a compareAndSet that decrements the state by 1 — atomically, without a lock. When the state hits 0, all threads parked in await() are unblocked via AQS's release mechanism. That's it. There is no reset path in the API. The latch is a one-way gate.

This single-use nature is a feature, not a limitation. It makes CountDownLatch perfect for start-up sequencing (wait for N services to register before opening traffic), test coordination (wait for N worker threads to complete before asserting results), and event broadcasting (all waiting threads unblock simultaneously the moment the count hits zero).

The key mental model: the thread calling await() is the dependent — it needs work done. The threads calling countDown() are the producers — they signal completion. These roles can overlap; a thread can countDown() and then await() on a different latch, which is exactly how two-phase startup coordination is built.

ServiceStartupCoordinator.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Simulates a service that must wait for three dependent sub-services
 * (database, cache, and message broker) to finish initialising before
 * it opens its own HTTP listener.
 */
public class ServiceStartupCoordinator {

    // Three sub-services must signal readiness before the main service starts.
    private static final int DEPENDENCY_COUNT = 3;

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch readinessLatch = new CountDownLatch(DEPENDENCY_COUNT);
        ExecutorService startupPool = Executors.newFixedThreadPool(DEPENDENCY_COUNT);

        // Each Runnable simulates a sub-service initialising and then
        // decrementing the latch to signal it is ready.
        startupPool.submit(new SubServiceInitialiser("DatabasePool",   1200, readinessLatch));
        startupPool.submit(new SubServiceInitialiser("RedisCache",      400, readinessLatch));
        startupPool.submit(new SubServiceInitialiser("MessageBroker",   800, readinessLatch));

        System.out.println("[MainThread] Waiting for all dependencies to become ready...");

        // await() parks the main thread inside AQS until the internal state reaches 0.
        // Using a timeout is critical in production — never block forever.
        boolean allReady = readinessLatch.await(5, TimeUnit.SECONDS);

        if (allReady) {
            System.out.println("[MainThread] All dependencies ready. Opening HTTP listener on :8080");
        } else {
            // This branch fires if one sub-service hangs past the timeout.
            System.err.println("[MainThread] Startup timeout! Shutting down safely.");
        }

        startupPool.shutdown();
    }

    static class SubServiceInitialiser implements Runnable {
        private final String serviceName;
        private final long   initDelayMs;    // Simulates different boot times
        private final CountDownLatch latch;

        SubServiceInitialiser(String serviceName, long initDelayMs, CountDownLatch latch) {
            this.serviceName  = serviceName;
            this.initDelayMs  = initDelayMs;
            this.latch        = latch;
        }

        @Override
        public void run() {
            try {
                System.out.printf("[%s] Initialising...%n", serviceName);
                Thread.sleep(initDelayMs);  // Simulate IO-bound startup work
                System.out.printf("[%s] Ready. Counting down.%n", serviceName);

                // countDown() is atomic — safe to call from multiple threads simultaneously.
                // It NEVER throws; even calling it when count is already 0 is a no-op.
                latch.countDown();

            } catch (InterruptedException e) {
                // Restore the interrupt flag — never swallow InterruptedException silently.
                Thread.currentThread().interrupt();
                System.err.printf("[%s] Interrupted during initialisation.%n", serviceName);
                // Still count down so the main thread isn't left waiting forever.
                latch.countDown();
            }
        }
    }
}
▶ Output
[MainThread] Waiting for all dependencies to become ready...
[DatabasePool] Initialising...
[RedisCache] Initialising...
[MessageBroker] Initialising...
[RedisCache] Ready. Counting down.
[MessageBroker] Ready. Counting down.
[DatabasePool] Ready. Counting down.
[MainThread] All dependencies ready. Opening HTTP listener on :8080
⚠️
Watch Out: Always Use the Timeout Overload of await()await() with no arguments blocks forever. In production, a crashed sub-service will never call countDown(), parking your main thread indefinitely. Use await(long timeout, TimeUnit unit) and handle the false return — it's the difference between a graceful degradation and a hung service with no stack trace to diagnose.

CyclicBarrier — Reusable Phases, the Barrier Action, and Its AQS Internals

CyclicBarrier is built differently from CountDownLatch. It uses an internal ReentrantLock and a Condition to park threads rather than AQS directly. The critical state is a 'generation' object that gets replaced each time the barrier trips (resets). This generation mechanism is precisely what makes the barrier cyclic — each trip through the barrier starts a fresh generation, so the same CyclicBarrier instance coordinates an unbounded number of phases.

The constructor accepts an optional Runnable barrierAction. This action runs exactly once per cycle, in the last thread to arrive at the barrier, before any of the waiting threads are released. This is incredibly useful for aggregating results from the phase that just completed (e.g., merging partial sums) before the next phase begins — all without an external synchronisation step.

Broken barrier state is a crucial concept you must understand. If any thread waiting at a barrier is interrupted or times out, the barrier enters a broken state. Every thread currently waiting — and every thread that calls await() on that barrier in the future — gets a BrokenBarrierException. The only recovery is to build a new CyclicBarrier. This failure mode is intentional: a partially-completed phase in iterative work produces corrupt results, so it's better to fail loudly.

Use CyclicBarrier for parallel iterative algorithms (matrix multiplication phases, parallel merge sort stages), simulation loops where N agent threads must sync before each tick, and multi-stage data-processing pipelines where every worker must finish stage N before any starts stage N+1.

ParallelMatrixRowProcessor.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.Arrays;

/**
 * Demonstrates a two-phase parallel computation.
 * Phase 1: Each worker thread computes the row-sum for its assigned matrix row.
 * Phase 2: After all row-sums are ready, each worker uses the global total
 *          to normalise its own row-sum.
 *
 * The CyclicBarrier ensures Phase 2 never starts until Phase 1 is 100% done.
 */
public class ParallelMatrixRowProcessor {

    private static final int ROW_COUNT    = 4;  // One worker thread per row
    private static final int COLUMN_COUNT = 5;

    // Shared result arrays — workers write here, the barrier action reads here.
    private static final int[]    rowSums     = new int[ROW_COUNT];
    private static       int      globalTotal = 0;  // Set by barrier action

    // Sample matrix — in practice this would be loaded from a data source.
    private static final int[][] matrix = {
        {1,  2,  3,  4,  5},
        {6,  7,  8,  9, 10},
        {11, 12, 13, 14, 15},
        {16, 17, 18, 19, 20}
    };

    public static void main(String[] args) throws InterruptedException {

        // The barrier action runs in the LAST thread to arrive.
        // It aggregates all row-sums into a global total before Phase 2 starts.
        Runnable aggregateRowSums = () -> {
            globalTotal = Arrays.stream(rowSums).sum();
            System.out.printf("%n[BarrierAction] All row sums computed. Global total = %d. Releasing Phase 2.%n%n", globalTotal);
        };

        // A single CyclicBarrier instance coordinates BOTH phases.
        // After it trips once (end of Phase 1), it resets automatically for Phase 2.
        CyclicBarrier phaseBarrier = new CyclicBarrier(ROW_COUNT, aggregateRowSums);

        ExecutorService workerPool = Executors.newFixedThreadPool(ROW_COUNT);

        for (int rowIndex = 0; rowIndex < ROW_COUNT; rowIndex++) {
            workerPool.submit(new RowProcessor(rowIndex, phaseBarrier));
        }

        workerPool.shutdown();
    }

    static class RowProcessor implements Runnable {
        private final int           rowIndex;
        private final CyclicBarrier phaseBarrier;

        RowProcessor(int rowIndex, CyclicBarrier phaseBarrier) {
            this.rowIndex     = rowIndex;
            this.phaseBarrier = phaseBarrier;
        }

        @Override
        public void run() {
            try {
                // ── PHASE 1: Compute this row's sum ──────────────────────────────────
                int sum = 0;
                for (int col = 0; col < COLUMN_COUNT; col++) {
                    sum += matrix[rowIndex][col];
                }
                rowSums[rowIndex] = sum;  // Write result to shared array
                System.out.printf("[Row-%d] Phase 1 done. Row sum = %d. Waiting at barrier.%n", rowIndex, sum);

                // await() decrements the internal count. When the last thread
                // arrives, the barrier action fires, THEN all threads are released.
                phaseBarrier.await();

                // ── PHASE 2: Normalise using the global total ─────────────────────────
                // At this point, globalTotal is guaranteed to be fully populated
                // because the barrier action completed before this line runs.
                double normalisedShare = (double) rowSums[rowIndex] / globalTotal * 100.0;
                System.out.printf("[Row-%d] Phase 2 done. Share of total = %.2f%%%n", rowIndex, normalisedShare);

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.printf("[Row-%d] Interrupted.%n", rowIndex);
            } catch (BrokenBarrierException e) {
                // Another thread was interrupted or timed out — the barrier is broken.
                // Log and exit cleanly; do NOT proceed with partial data.
                System.err.printf("[Row-%d] Barrier broken — aborting phase processing.%n", rowIndex);
            }
        }
    }
}
▶ Output
[Row-0] Phase 1 done. Row sum = 15. Waiting at barrier.
[Row-1] Phase 1 done. Row sum = 40. Waiting at barrier.
[Row-2] Phase 1 done. Row sum = 65. Waiting at barrier.
[Row-3] Phase 1 done. Row sum = 90. Waiting at barrier.

[BarrierAction] All row sums computed. Global total = 210. Releasing Phase 2.

[Row-3] Phase 2 done. Share of total = 42.86%
[Row-0] Phase 2 done. Share of total = 7.14%
[Row-2] Phase 2 done. Share of total = 30.95%
[Row-1] Phase 2 done. Share of total = 19.05%
⚠️
Pro Tip: The Barrier Action Runs in the Last-Arriving ThreadIf your barrier action throws an unchecked exception, it propagates to the last thread and all other waiting threads receive a BrokenBarrierException. Keep the barrier action lightweight and exception-safe. Do your heavy aggregation work there but wrap it in a try-catch — a crashed barrier action poisons the entire cycle silently if you're not watching.

Head-to-Head Comparison — Choosing the Right Tool Under Pressure

The single most important question to ask yourself is: 'Is the wait one-directional (waiters depend on workers) or mutual (everyone waits for everyone)?' CountDownLatch is one-directional. CyclicBarrier is mutual.

The second question is: 'Does this pattern repeat?' If threads need to sync once and move on independently, use CountDownLatch. If threads must sync at the end of every phase in a loop, CyclicBarrier's automatic reset is exactly what you need — recreating a CountDownLatch every iteration is wasteful and error-prone.

Performance considerations matter at scale. CountDownLatch.countDown() is a single CAS on an AQS integer — extremely cheap. CyclicBarrier.await() acquires a ReentrantLock, which involves more overhead. For ultra-hot paths with thousands of threads syncing per second, consider Phaser (the more flexible successor to both) which uses a tree-structured internal state to reduce contention. For most application-level coordination (tens of threads, not thousands), both primitives are fast enough that the design clarity matters far more than the performance difference.

Error propagation also differs sharply. A failed countDown() call (e.g., from a crashed thread that never calls it) simply leaves the latch stuck — which is why the timeout overload of await() is non-negotiable in production. CyclicBarrier's broken-barrier state at least actively notifies waiting threads that something went wrong, making it somewhat easier to detect a fault mid-cycle.

PhaserMigrationHint.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445
import java.util.concurrent.Phaser;

/**
 * Quick illustration of Phaser as the flexible upgrade path.
 * Unlike CyclicBarrier, Phaser supports dynamic participant registration
 * and per-phase arrival tracking — useful when the number of workers
 * isn't known at construction time.
 *
 * This is NOT a replacement example — it's a pointer for when you've
 * outgrown both CountDownLatch and CyclicBarrier.
 */
public class PhaserMigrationHint {

    public static void main(String[] args) throws InterruptedException {

        // Phaser starts with 1 participant — the main thread (the 'overseer').
        Phaser overseerPhaser = new Phaser(1);

        for (int workerIndex = 0; workerIndex < 3; workerIndex++) {
            final int id = workerIndex;

            // Each worker registers itself dynamically — no fixed count at construction.
            overseerPhaser.register();

            Thread worker = new Thread(() -> {
                System.out.printf("[Worker-%d] Arriving at phase %d%n", id, overseerPhaser.getPhase());

                // arriveAndAwaitAdvance is the CyclicBarrier.await() equivalent.
                // Returns the phase number AFTER the barrier trips.
                overseerPhaser.arriveAndAwaitAdvance();

                System.out.printf("[Worker-%d] Phase advanced. Continuing.%n", id);

                // Deregister when done — reduces participant count for future phases.
                overseerPhaser.arriveAndDeregister();
            });
            worker.start();
        }

        // Main thread arrives — this is the last arrival that trips the phase.
        overseerPhaser.arriveAndDeregister();

        System.out.println("[Main] All workers released. Phaser terminated: " + overseerPhaser.isTerminated());
    }
}
▶ Output
[Worker-0] Arriving at phase 0
[Worker-1] Arriving at phase 0
[Worker-2] Arriving at phase 0
[Worker-0] Phase advanced. Continuing.
[Worker-1] Phase advanced. Continuing.
[Worker-2] Phase advanced. Continuing.
[Main] All workers released. Phaser terminated: true
🔥
Interview Gold: Know When to Mention PhaserInterviewers love it when you voluntarily bring up Phaser after explaining CyclicBarrier. Say: 'If the participant count is dynamic, or if I need per-phase callbacks with richer lifecycle control, I'd reach for Phaser instead.' It signals you actually use these APIs in anger, not just read about them.
Feature / AspectCountDownLatchCyclicBarrier
Reusable after trippingNo — single use onlyYes — resets automatically each cycle
Who waitsOne or more designated waiter threadsAll participant threads wait for each other
Internal synchroniserAQS (compareAndSet on state integer)ReentrantLock + Condition + generation object
Barrier/trip actionNot supportedOptional Runnable runs in last-arriving thread
Failed-thread behaviourLatch stays stuck (timeout is your safety net)Barrier enters broken state; BrokenBarrierException thrown to all
Dynamic participant countNot supportedNot supported (use Phaser instead)
Primary use caseStart-up sequencing, one-time event signallingIterative phase synchronisation, parallel algorithms
Thread rolesWaiters vs. workers (distinct roles)All threads are both workers and waiters
Performance overheadVery low — single CAS per countDown()Higher — ReentrantLock acquisition per await()
Available sinceJava 5 (java.util.concurrent)Java 5 (java.util.concurrent)

🎯 Key Takeaways

  • CountDownLatch is a one-shot gate: once the count hits zero it stays open forever and cannot be reset — design your code around this or pick a different tool.
  • CyclicBarrier's broken-barrier state is a feature: it actively poisons all waiting threads when one fails, preventing silent partial-phase execution that would corrupt iterative results.
  • The barrier action in CyclicBarrier runs in the last-arriving thread before any waiting threads are released — exploit this for zero-overhead phase aggregation without an extra synchronisation step.
  • When participant count is unknown at construction time, or you need tiered phase callbacks, Phaser is the right upgrade path — mentioning it unprompted in an interview signals genuine production experience.

⚠ Common Mistakes to Avoid

  • Mistake 1: Calling countDown() inside a try block without a finally guarantee — If the code between entering the thread and calling countDown() throws an unchecked exception, countDown() is skipped and the latch count never reaches zero, permanently blocking the awaiting thread. Fix: always call countDown() in a finally block so it fires regardless of whether the worker succeeded or failed.
  • Mistake 2: Catching InterruptedException and not restoring the interrupt flag, then failing to countDown()/signal the barrier — The thread swallows the interrupt silently, neither unblocking itself nor releasing other waiting threads, causing a deadlock that's invisible in stack traces. Fix: always call Thread.currentThread().interrupt() after catching InterruptedException, and still call countDown() or let the barrier handle cleanup via the broken-state mechanism.
  • Mistake 3: Reusing a CountDownLatch by wrapping it in a new allocation inside a loop — Developers create a new CountDownLatch(n) at the top of each loop iteration thinking they're 'resetting' it, but if any reference from a previous iteration is still held by a thread, it points to the old exhausted latch, leading to immediate await() returns on stale state. Fix: if you need a reusable countdown gate, switch to CyclicBarrier or Phaser from the start — they are purpose-built for this pattern and the API makes the intent explicit.

Interview Questions on This Topic

  • QCan you explain the difference between CountDownLatch and CyclicBarrier, and give a concrete production scenario where you'd pick one over the other?
  • QWhat happens to a CyclicBarrier if one of the waiting threads is interrupted? How does that affect the other threads, and how would you recover?
  • QIf CountDownLatch.countDown() is called more times than the initial count — say the latch was created with count 3 and countDown() is called 5 times — what happens? And how does that differ from CyclicBarrier.await() being called by more threads than the barrier's party count?

Frequently Asked Questions

Can a CountDownLatch count back up after reaching zero?

No. Once a CountDownLatch reaches zero it is permanently open. There is no reset or increment method in the API — this is by design. If you need a resettable gate, use CyclicBarrier (fixed participant count) or Phaser (dynamic participant count).

What is the BrokenBarrierException in Java and when does it get thrown?

BrokenBarrierException is thrown to any thread calling CyclicBarrier.await() when the barrier is in a broken state. The barrier breaks if any waiting thread is interrupted, if any waiting thread times out via the await(long, TimeUnit) overload, or if the barrier action throws an exception. Once broken, the barrier stays broken — you must construct a new instance to recover.

Is it safe to call CountDownLatch.countDown() from multiple threads at the same time?

Yes, completely. countDown() performs a single atomic compareAndSet operation on the internal AQS state, making it inherently thread-safe with no locks. Multiple threads can call it simultaneously without any external synchronisation.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousAtomic Classes in JavaNext →Observer Pattern in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged