CountDownLatch & CyclicBarrier in Java: Coordinating Thread Groups
Java provides high-level synchronizers for coordinating groups of threads. CountDownLatch lets one or more threads wait until a set of operations completes: it counts down to zero and cannot be reset. CyclicBarrier makes a fixed group of threads wait for each other at a common barrier point and can be reused across multiple phases.
CountDownLatch vs CyclicBarrier: Key Differences
| Property | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusable | No: count reaches zero and stays there | Yes: resets automatically after each barrier trip |
| Who counts | Any thread calls countDown() | Participating threads call await() |
| Who waits | Threads blocked on await() | All threads wait at the barrier until last arrives |
| Barrier action | None | Optional Runnable runs when barrier trips |
| Use case | Signal when N events have occurred | Synchronise N threads at a checkpoint |
| Exception handling | Interrupted threads propagate InterruptedException | One broken thread puts barrier in broken state for all |
CountDownLatch: Wait Until N Events Complete
A CountDownLatch is initialised with a count. Each call to countDown() decrements it. Threads blocked on await() are released when the count reaches zero.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LatchExample {
public static void main(String[] args) throws InterruptedException {
int workerCount = 5;
CountDownLatch latch = new CountDownLatch(workerCount);
ExecutorService pool = Executors.newFixedThreadPool(workerCount);
for (int i = 0; i < workerCount; i++) {
final int id = i;
pool.submit(() -> {
try {
System.out.println("Worker " + id + " starting");
Thread.sleep(200 + id * 100); // simulate work
System.out.println("Worker " + id + " done");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // always decrement, even on exception
}
});
}
System.out.println("Main thread waiting for all workers...");
latch.await(); // blocks until count reaches 0
System.out.println("All workers finished — proceeding!");
pool.shutdown();
}
}
countDown() in a finally block so the latch is decremented even if the worker throws an exception. A missed countDown() will cause await() to block forever.
Timed await: Avoid Blocking Forever
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
CountDownLatch latch = new CountDownLatch(3);
// ... start workers ...
// Wait at most 5 seconds
boolean completed = latch.await(5, TimeUnit.SECONDS);
if (completed) {
System.out.println("All tasks finished in time");
} else {
System.out.println("Timeout — not all tasks completed");
}
// Check remaining count without waiting
System.out.println("Remaining: " + latch.getCount());
Real-World: Parallel Data Fetch & Aggregation
A common pattern: fan out N parallel fetches, then aggregate results once all are done.
import java.util.concurrent.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ParallelFetch {
record UserData(int id, String name) {}
public static List<UserData> fetchAll(List<Integer> userIds) throws InterruptedException {
List<UserData> results = new CopyOnWriteArrayList<>();
CountDownLatch latch = new CountDownLatch(userIds.size());
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); // Java 21
for (int id : userIds) {
pool.submit(() -> {
try {
UserData data = fetchFromDb(id); // remote call
results.add(data);
} finally {
latch.countDown();
}
});
}
latch.await(10, TimeUnit.SECONDS);
pool.shutdown();
return results;
}
private static UserData fetchFromDb(int id) {
// simulate DB round-trip
return new UserData(id, "User-" + id);
}
}
CyclicBarrier: Synchronise Threads at a Checkpoint
All participating threads call await() and block until the last thread arrives. Then all are released simultaneously and the barrier resets for the next round.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BarrierExample {
public static void main(String[] args) {
int parties = 3;
// Optional barrier action runs in the last thread to arrive
Runnable barrierAction = () ->
System.out.println("--- All threads reached barrier; starting next phase ---");
CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction);
ExecutorService pool = Executors.newFixedThreadPool(parties);
for (int phase = 1; phase <= 3; phase++) {
final int p = phase;
for (int i = 0; i < parties; i++) {
final int id = i;
pool.submit(() -> {
try {
System.out.printf("Thread %d phase %d working%n", id, p);
Thread.sleep((long)(Math.random() * 300));
barrier.await(); // wait for all peers
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
});
}
}
pool.shutdown();
}
}
CyclicBarrier resets automatically after each barrier trip, making it ideal for iterative algorithms (parallel merge sort phases, Monte Carlo simulation rounds, game loop tick synchronisation).
Handling a Broken Barrier
If one thread times out or is interrupted while waiting, the barrier enters a broken state. All threads waiting (or about to wait) receive BrokenBarrierException. Call barrier.reset() to recover, but only if no threads are currently waiting.
import java.util.concurrent.*;
CyclicBarrier barrier = new CyclicBarrier(3);
Runnable task = () -> {
try {
// Timed wait — prevents hanging if a peer crashes
barrier.await(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Timed out waiting at barrier");
} catch (BrokenBarrierException e) {
System.out.println("Barrier broken by another thread");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// Check barrier state
System.out.println("Broken: " + barrier.isBroken());
System.out.println("Parties waiting: " + barrier.getNumberWaiting());
// Reset (only safe when no threads are waiting)
if (barrier.isBroken()) {
barrier.reset();
}
barrier.reset() while threads are waiting causes those threads to receive BrokenBarrierException. Only reset when the barrier is not in active use.
Choosing Between CountDownLatch and CyclicBarrier
| Scenario | Use |
|---|---|
| Wait for N services to start before accepting traffic | CountDownLatch |
| Wait for N tasks to complete before proceeding once | CountDownLatch |
| Signal a gate: one thread opens it, N threads rush through | CountDownLatch(1) |
| Parallel algorithm with multiple synchronisation rounds | CyclicBarrier |
| Run a barrier action (merge partial results) after each phase | CyclicBarrier + barrierAction |
| Game loop: all players must submit moves before next tick | CyclicBarrier |