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.

Share

CountDownLatch vs CyclicBarrier: Key Differences

PropertyCountDownLatchCyclicBarrier
ReusableNo: count reaches zero and stays thereYes: resets automatically after each barrier trip
Who countsAny thread calls countDown()Participating threads call await()
Who waitsThreads blocked on await()All threads wait at the barrier until last arrives
Barrier actionNoneOptional Runnable runs when barrier trips
Use caseSignal when N events have occurredSynchronise N threads at a checkpoint
Exception handlingInterrupted threads propagate InterruptedExceptionOne 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.

Java
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();
    }
}
Always call 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

Java
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.

Java
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.

Java
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();
    }
}
The 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.

Java
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();
}
Calling 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

ScenarioUse
Wait for N services to start before accepting trafficCountDownLatch
Wait for N tasks to complete before proceeding onceCountDownLatch
Signal a gate: one thread opens it, N threads rush throughCountDownLatch(1)
Parallel algorithm with multiple synchronisation roundsCyclicBarrier
Run a barrier action (merge partial results) after each phaseCyclicBarrier + barrierAction
Game loop: all players must submit moves before next tickCyclicBarrier