CountDownLatch is a one-shot gate. It starts with a count; threads calling await() block until the count reaches zero, driven down by countDown() calls from other threads. It answers exactly one question — 'have all N things happened yet?' — and once the count hits zero it is spent and cannot be reused. That simplicity is the point: it is the cleanest way to make one thread wait for a set of others to finish.
The completion-gate pattern
The common use: a coordinator waits for a fixed number of worker tasks to complete. Initialise the latch to the worker count; each worker counts down as it finishes; the coordinator awaits zero.
CountDownLatch done = new CountDownLatch(workers);
for (int i = 0; i < workers; i++) {
executor.submit(() -> {
try { doWork(); }
finally { done.countDown(); } // count down even on failure
});
}
done.await(); // blocks until all workers have counted down
System.out.println("all workers finished");The countDown() goes in a finally: if a worker throws and skips it, the count never reaches zero and the coordinator waits forever. This is the number-one CountDownLatch bug.
The start-gate pattern
A latch initialised to 1 is a release valve: many threads await it, and a single countDown() lets them all proceed at once. This is how you make N threads start simultaneously — useful for load tests where you want true concurrency, not staggered starts.
CountDownLatch start = new CountDownLatch(1);
for (Runnable r : tasks) new Thread(() -> {
start.await(); // all threads park here
r.run(); // ...and are released together
}).start();
start.countDown(); // fire the starting gunCombine the two — a start latch of 1 and a done latch of N — and you can measure the wall-clock time of N tasks running genuinely in parallel.
await with a timeout
await(timeout, unit) returns a boolean: true if the count reached zero in time, false if it timed out. This prevents a hung worker from blocking the coordinator indefinitely — you decide what a partial completion means.
if (!done.await(30, TimeUnit.SECONDS)) {
long remaining = done.getCount();
log.warn("{} workers did not finish in time", remaining);
}getCount() reports the current count — handy for diagnostics, though you should not use it for control flow (it is a moving target under concurrency).
Why it cannot be reset
A CountDownLatch is deliberately single-use: there is no way to raise the count back up. Once it is at zero, every await() returns immediately, forever. If you need a barrier that resets and reused across rounds, that is a different tool: CyclicBarrier for a fixed party count looping through phases, or Phaser when the party count changes. Trying to reuse a spent latch is a category error — reach for the cyclic tools instead.
Memory visibility guarantee
Beyond coordination, the latch provides a happens-before edge: everything a worker did before its countDown() is guaranteed visible to the coordinator after await() returns. So the coordinator can safely read results the workers wrote — into a concurrent collection, or even into plain fields whose publication is covered by the latch — without additional synchronization. This is why a latch is not just a timing tool but a safe results-handoff mechanism.
CountDownLatch is a one-shot gate: a count driven to zero by countDown(), with await() blocking until then. Use it as a completion gate (init to N, count down in finally) or a start gate (init to 1, release all at once). It cannot reset — use CyclicBarrier/Phaser for that — and it establishes happens-before, so worker results are safely visible to the awaiter.