CountDownLatch Deadlock — Missing countDown() After Crash
One unchecked exception before countDown() hangs your service forever.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- CountDownLatch is a one-shot gate: thread(s) wait until count reaches zero, then stays open forever.
- CyclicBarrier is a reusable meeting point: all threads wait for each other, then reset for next cycle.
- CountDownLatch uses AQS with a single CAS per countDown(); CyclicBarrier uses ReentrantLock + Condition, higher overhead.
- In production, always use await(timeout) with CountDownLatch; a hung worker blocks indefinitely.
- CyclicBarrier's broken barrier state is active failure detection; CountDownLatch just stays stuck silently.
- Biggest mistake: using CountDownLatch when you need reuse, or CyclicBarrier when participants are dynamic.
CountDownLatch and CyclicBarrier are Java's go-to concurrency synchronizers for coordinating threads, but they solve fundamentally different problems. CountDownLatch is a one-shot gate: you set a count, threads decrement it via countDown(), and one or more threads block on until the count hits zero.await()
It's designed for scenarios like waiting for N services to start, N tasks to complete, or a single signal to fire. Once the latch reaches zero, it's permanently open — no reset. CyclicBarrier, by contrast, is a reusable rendezvous point: N threads call , and when all arrive, they all proceed simultaneously.await()
It optionally runs a barrier action (a Runnable) at that point, then resets for the next cycle. This makes it ideal for phased computations like iterative algorithms, batch processing, or multi-stage simulations where threads synchronize at phase boundaries.
Under the hood, both use AbstractQueuedSynchronizer (AQS), but CountDownLatch uses a shared acquire/release mode with a state representing the count, while CyclicBarrier uses a ReentrantLock and Condition internally (not AQS directly) to manage the generation and reset logic. The critical difference in practice: a missing countDown() after a crash in CountDownLatch causes permanent deadlock — threads waiting on never wake.await()
CyclicBarrier handles thread failure more gracefully via a broken barrier exception, but misuse (e.g., forgetting to catch exceptions in the barrier action) can leave it permanently broken. Choose CountDownLatch for one-shot coordination where you control the count precisely; choose CyclicBarrier for repeated synchronization phases where you need reset capability and fault detection.
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 vs CyclicBarrier — Two Synchronizers, One Critical Difference
CountDownLatch and CyclicBarrier are both Java synchronizers that coordinate multiple threads, but they solve fundamentally different problems. CountDownLatch is a one-shot gate: one or more threads block until a fixed number of countDown() calls have been made. CyclicBarrier is a reusable rendezvous point: a fixed number of threads all wait for each other to arrive, then proceed together.
CountDownLatch is not reusable — once the count reaches zero, the latch is permanently open. CyclicBarrier resets automatically after all parties trip it, and can optionally run a barrier action. Both operate in O(1) time per operation under the hood, using AQS (AbstractQueuedSynchronizer). The practical difference: CountDownLatch signals an event; CyclicBarrier synchronizes a phase.
Use CountDownLatch when you need to wait for N operations to complete before proceeding — e.g., waiting for N services to start, or N parallel tasks to finish. Use CyclicBarrier when you have a fixed-size group of threads that must meet at a common point repeatedly — e.g., in parallel simulations or multi-phase computations. The wrong choice leads to deadlocks or wasted threads.
CountDownLatch.await().await() to detect stuck latches.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.
package io.thecodeforge.concurrent; 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(); } } } }
await() and handle the false return every single time.await() without a timeout.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.
package io.thecodeforge.concurrent; 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); } } } }
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.
package io.thecodeforge.concurrent; 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()); } }
Production Decision Framework — How to Pick the Right Primitive
You've seen the internals. Now here's a concrete decision tree you can apply in code reviews or on the whiteboard. Start with these three questions:
1. Roles: Are there distinct 'waiters' and 'workers', or does every thread play both roles?** - Distinct roles → CountDownLatch. One thread (or group) waits for others to finish. - All threads equal → CyclicBarrier. Everyone waits for everyone.
2. Repeatability: Will this coordination point be used exactly once, or multiple times?** - Once → CountDownLatch (or create a new one each time, but that's fragile). - Multiple times → CyclicBarrier (auto-reset) or Phaser (if participants change).
3. Failure semantics: What should happen if a worker fails?** - Silent stuck latch? Use CountDownLatch with timeout. - Active failure notification? Use CyclicBarrier — BrokenBarrierException tells all threads.
Use this table as a quick reference:
| Scenario | Best Choice | Why |
|---|---|---|
| Start-up sequencing (wait for N services) | CountDownLatch | One-shot, distinct roles |
| Parallel algorithm with phases | CyclicBarrier | Reusable, all threads equal |
| Test coordination (wait for threads to finish) | CountDownLatch | Simple, one-time |
| Dynamic worker pool for iterative processing | Phaser | Participants can join/leave |
| Event broadcasting (fire when all ready) | CountDownLatch | All waiters unblock simultaneously |
| Simulation ticks where each tick is a phase | CyclicBarrier | Auto-reset, barrier action for aggregation |
In production, apply the rule of least surprise: pick the primitive whose name and contract clearly communicate the intent. Your future self — and your colleagues — will thank you.
- Gate: Opens once. Once open, nothing stops it. Perfect for one-time dependencies.
- Round table: Everyone sits, the barrier action runs (like a toast), then they get up and the table resets for the next course.
- Phaser extends the round table: chairs can be added or removed between courses.
await() hangs.Common Pitfalls and How to Avoid Them
Even experienced developers make these mistakes. Here's what to watch for.
Pitfall 1: Missing countDown() guarantee If your worker code throws an unchecked exception before calling countDown(), the latch never reaches zero. Always wrap the body in try-finally and call countDown() in the finally block. This ensures the latch is decremented even on failure.
Pitfall 2: Forgeting to restore the interrupt flag When you catch InterruptedException, you must call Thread.currentThread().interrupt() to reassert the interrupt. Failure to do so leaves the thread in a state that can't be cancelled, and if that thread is waiting on a CyclicBarrier, it never breaks the barrier — leading to a deadlock.
Pitfall 3: Reusing a CountDownLatch by creating a new one in a loop You create a new CountDownLatch(n) each iteration, but if a reference from a previous iteration is still held by another thread, that latch is exhausted and await() returns immediately. Switch to CyclicBarrier or Phaser if you need reuse.
Pitfall 4: Calling countDown() after the latch has reached zero It's a no-op, but it can mask bugs. For example, if you accidentally call countDown() 5 times on a latch initialised with 3, the extra calls do nothing — but you'll never know a worker was supposed to only run once. Add assertions if you suspect over-counting.
Pitfall 5: Using CyclicBarrier with more threads than the party count If you submit 5 workers but the barrier expects 4, the barrier will never trip because the 5th thread's await() doesn't count? Actually it does – if you submit extra threads that also call await(), they increase the effective party? No, the barrier waits for exactly its party count. If 5 threads call await() on a 4-part barrier, one thread will be left waiting forever. Ensure threads == barrier parties exactly, or use a secondary coordination mechanism.
Pitfall 6: Not handling BrokenBarrierException If you ignore BrokenBarrierException and continue, you risk processing garbage data. Always abort the current phase and restart with a fresh barrier.
CyclicBarrier.await(), it breaks the barrier. But if you catch InterruptedException and don't restore the flag, the barrier stays broken and other threads get BrokenBarrierException. Always call Thread.currentThread().interrupt() — otherwise you mask the interruption and the barrier recovery is incomplete.await() from an abandoned phase.await().Why CountDownLatch Cares About Tasks, Not Threads
That distinction kills more junior engineers than null pointers. CountDownLatch tracks a counter you decrement. It has zero interest in who does the decrementing. One thread can call countDown() five times. Five threads can each call it once. The latch doesn't care. It only watches that counter hit zero. This matters in production because you might have a thread pool of three workers needing to complete eight pre-flight checks. With CountDownLatch, you set the initial count to eight, each check calls countDown(), and your coordinator thread waits on await(). The pool size is irrelevant. CyclicBarrier would force you to match thread count to barrier count, which is the wrong abstraction when you're tracking units of work, not thread rendezvous. That's the root cause I've debugged at 2 AM: someone treated the barrier count like a task counter and wondered why their pipeline deadlocked. Don't be that engineer.
// io.thecodeforge import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; public class PreflightCoordinator { public void run() throws InterruptedException { var latch = new CountDownLatch(8); // eight checks, not eight threads var executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 8; i++) { executor.submit(() -> { try { Thread.sleep(200); } catch (InterruptedException e) {} latch.countDown(); // each task calls countDown }); } latch.await(); // coordinator waits for all eight System.out.println("All preflight checks passed. Launching."); executor.shutdown(); } }
await().Reusability Is Where CyclicBarrier Earns Its Paycheck
CountDownLatch is a one-shot. Once you hit zero, it's a corpse. You cannot reset it. CyclicBarrier resets implicitly when all parties trip the barrier, or explicitly via reset(). That reusability defines when you reach for it. Think phased computations: map stage, then reduce stage, then output stage. Each phase needs all threads to sync before the next. You create one CyclicBarrier with your phase count, call await() at the end of each phase, and optionally run a barrier action (like shuffling data) between phases. The barrier handles reset automatically. In production, I've used this for partitioned cache refresh jobs: each partition loads fresh data, threads rendezvous, then the barrier action publishes the combined update. Without CyclicBarrier, you'd be wiring CountDownLatch phantoms, resetting them manually, and praying you don't leak a reference. That's fragile. CyclicBarrier is built for that rhythm.
// io.thecodeforge import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class PhasedCacheRefresh { private static final int PARTITIONS = 4; private final CyclicBarrier barrier; private String[] partitionData = new String[PARTITIONS]; public PhasedCacheRefresh() { barrier = new CyclicBarrier(PARTITIONS, () -> { System.out.println("Merging partition data across all threads"); // barrier action: publish combined cache update }); } public void refreshPartition(int partitionId) { partitionData[partitionId] = "fresh-data-" + partitionId; try { barrier.await(); // sync point // after barrier, all partitions have fresh data } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); } } }
Startup Hang: Missing countDown() After Worker Crash
CountDownLatch.await().await() with no timeout. Latch count stayed at 1 forever.await().- Never call
await()without a timeout in production. - Put countDown() in a finally block – every time.
- If a worker crashes before decrementing, your latch becomes a deadlock trap.
CountDownLatch.await()await() again. Broken barriers require a new instance.jstack $(pgrep -f 'your-app') | grep -A 20 'CountDownLatch.await'Check latch count: add debug logging or attach with jcmd to inspect objectgrep -r 'BrokenBarrierException' /var/log/app/ | tail -50jstack <pid> | grep -A 15 'CyclicBarrier.doWait'await() to prevent indefinite hangjstack <pid> | grep -A 10 'Phaser'Verify registered parties: add Phaser.getRegisteredParties() logarrive() or arriveAndDeregister(); use timeout in awaitAdvance()| Feature / Aspect | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusable after tripping | No — single use only | Yes — resets automatically each cycle |
| Who waits | One or more designated waiter threads | All participant threads wait for each other |
| Internal synchroniser | AQS (compareAndSet on state integer) | ReentrantLock + Condition + generation object |
| Barrier/trip action | Not supported | Optional Runnable runs in last-arriving thread |
| Failed-thread behaviour | Latch stays stuck (timeout is your safety net) | Barrier enters broken state; BrokenBarrierException thrown to all |
| Dynamic participant count | Not supported | Not supported (use Phaser instead) |
| Primary use case | Start-up sequencing, one-time event signalling | Iterative phase synchronisation, parallel algorithms |
| Thread roles | Waiters vs. workers (distinct roles) | All threads are both workers and waiters |
| Performance overhead | Very low — single CAS per countDown() | Higher — ReentrantLock acquisition per await() |
| Available since | Java 5 (java.util.concurrent) | Java 5 (java.util.concurrent) |
| File | Command / Code | Purpose |
|---|---|---|
| io.thecodeforge.concurrent.ServiceStartupCoordinator.java | /** | CountDownLatch |
| io.thecodeforge.concurrent.ParallelMatrixRowProcessor.java | /** | CyclicBarrier |
| io.thecodeforge.concurrent.PhaserMigrationHint.java | /** | Head-to-Head Comparison |
| PreflightCoordinator.java | public class PreflightCoordinator { | Why CountDownLatch Cares About Tasks, Not Threads |
| PhasedCacheRefresh.java | public class PhasedCacheRefresh { | Reusability Is Where CyclicBarrier Earns Its Paycheck |
Key takeaways
await() variants in productionCommon mistakes to avoid
5 patternsNot guaranteeing countDown() in a finally block
Catching InterruptedException without restoring the interrupt flag
Thread.currentThread().interrupt() in the catch block for InterruptedException. For CountDownLatch, still call countDown() to unblock others. For CyclicBarrier, let await() throw BrokenBarrierException.Reusing a CountDownLatch by allocating a new one in a loop
Calling CyclicBarrier.await() without handling BrokenBarrierException
await() calls throw BrokenBarrierException. If the code catches Exception generically or ignores it, the phase may continue with partial data.Using CyclicBarrier with a thread pool that has dynamic size
await() varies (e.g., cached thread pool), the barrier may never trip because the party count is fixed. Extra threads calling await() exceed the intended count, or too few threads arrive.await().Interview Questions on This Topic
Can you explain the difference between CountDownLatch and CyclicBarrier, and give a concrete production scenario where you'd pick one over the other?
What happens to a CyclicBarrier if one of the waiting threads is interrupted? How does that affect the other threads, and how would you recover?
CyclicBarrier.await() is interrupted, the barrier immediately enters a broken state. All other waiting threads receive a BrokenBarrierException when they call await(). The barrier stays broken permanently — you must construct a new CyclicBarrier to recover. Recovery involves catching BrokenBarrierException, logging the failure, and restarting the phase with a fresh barrier instance.If 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?
await() with more threads than the party count will cause the barrier to trip when the exact number of parties arrive, leaving the extra threads waiting forever on a barrier that will never reset. This is a deadlock. Always ensure the number of threads calling await() equals the barrier's party count exactly.Explain the generation mechanism inside CyclicBarrier. Why is it important for correctness?
await() on the old generation immediately sees the broken flag and throws BrokenBarrierException. When the barrier resets normally, a new generation is created, so new cycles are unaffected by past failures. This design prevents stale state from corrupting future cycles.How would you implement a reusable countdown latch using CyclicBarrier? What are the limitations?
await() after completing their task. When all N workers have arrived, the barrier trips and the controller knows all tasks are done. However, this approach is convoluted — you're abusing CyclicBarrier's design. It's better to use Phaser or simply create a new CountDownLatch per iteration. The limitations include managing the controller thread and handling the barrier reset correctly.Frequently Asked Questions
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).
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.
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.
No. Once a CyclicBarrier enters a broken state, it stays broken permanently. Even if all threads receive BrokenBarrierException, the barrier will never reset. You must create a new CyclicBarrier instance to continue coordination.
Phaser is a more flexible version of CyclicBarrier. Key differences: Phaser supports dynamic participant registration (parties can join/leave at runtime), per-phase callbacks, and can be terminated or deregistered. CyclicBarrier requires a fixed number of parties at construction and has only one optional barrier action. Use Phaser when the number of threads is unknown or changes over time.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Multithreading. Mark it forged?
7 min read · try the examples if you haven't