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.

Advertisement

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 gun

Combine 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).

Advertisement

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.