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

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

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); } }

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

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(); } Calling barrier.reset() while threads are waiting causes those threads to receive BrokenBarrierException. Only reset when the barrier is not in active use.
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