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
// ── 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 0The Two Canonical Patterns
// ── 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
// ── 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