Coordinating Threads: CountDownLatch & CyclicBarrier
Part 3 of 8 in Java Concurrency: Deep Dive
CountDownLatch and CyclicBarrier both make threads wait for each other, but they don’t do it the same way. This post runs two small Java programs, each compiled and run for real, to show a latch actually blocking a waiting thread until worker threads finish, and a barrier actually firing its action on more than one round instead of just once.
CountDownLatch: the waiter really blocks
CountDownLatchDemo starts 3 worker threads that sleep for 300ms, 600ms, and 900ms respectively, then call latch.countDown() and print the time they did it. The main thread calls latch.await() right after starting them and prints the time await() actually returns.
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);
long start = System.nanoTime();
int[] sleepMs = {300, 600, 900};
for (int i = 0; i < workerCount; i++) {
final int id = i;
new Thread(() -> {
try {
Thread.sleep(sleepMs[id]);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long at = (System.nanoTime() - start) / 1_000_000;
System.out.println("worker " + id + " finished at " + at + "ms, counting down");
latch.countDown();
}).start();
}
System.out.println("main: waiting on latch.await()");
latch.await();
long resumedAt = (System.nanoTime() - start) / 1_000_000;
System.out.println("main: latch.await() returned at " + resumedAt + "ms");
}
}
Real javac+java output, JDK 25 (Zulu):
main: waiting on latch.await()
worker 0 finished at 305ms, counting down
worker 1 finished at 605ms, counting down
worker 2 finished at 905ms, counting down
main: latch.await() returned at 905ms
The main thread printed “waiting on latch.await()” immediately, then didn’t print again until 905ms — the same moment worker 2 (the slowest, at 905ms) counted down. Workers 0 and 1 finished earlier (305ms, 605ms) and counted down, but await() didn’t return until all three had — including the last one.
CyclicBarrier: the sync point resets
CyclicBarrierDemo starts 3 threads that each loop 3 rounds. In every round a thread sleeps (a different duration per thread, so they don’t arrive at the same instant), prints that it reached the barrier, then calls barrier.await(). The barrier was built with an action that prints once all 3 threads have arrived for that round.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
int rounds = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () ->
System.out.println("*** barrier action: all " + threadCount + " threads reached the barrier ***")
);
for (int i = 0; i < threadCount; i++) {
final int id = i;
new Thread(() -> {
try {
for (int round = 1; round <= rounds; round++) {
Thread.sleep(100 * (id + 1));
System.out.println("thread " + id + " reached barrier, round " + round);
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
Real javac+java output, JDK 25 (Zulu):
thread 0 reached barrier, round 1
thread 1 reached barrier, round 1
thread 2 reached barrier, round 1
*** barrier action: all 3 threads reached the barrier ***
thread 0 reached barrier, round 2
thread 1 reached barrier, round 2
thread 2 reached barrier, round 2
*** barrier action: all 3 threads reached the barrier ***
thread 0 reached barrier, round 3
thread 1 reached barrier, round 3
thread 2 reached barrier, round 3
*** barrier action: all 3 threads reached the barrier ***
The barrier action line — “all 3 threads reached the barrier” — printed 3 times, once after each round’s three threads arrived. The same CyclicBarrier instance was reused for round 2 and round 3 without creating a new one.
Takeaway
The CountDownLatch run showed the waiting thread genuinely blocked at await() until the slowest of the three workers (905ms) counted down, not returning early for the two that finished sooner. The CyclicBarrier run showed the same barrier instance firing its action 3 separate times, once per round, instead of being usable only once.