> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use CountDownLatch, CyclicBarrier and Semaphore
- URL: https://www.ggorantala.dev/how-to-use-countdownlatch-cyclicbarrier-semaphore/
- Published: 2026-07-06T05:12:00.000Z
- Updated: 2026-10-31T06:32:54.000Z
- Description: Build a stock-sync job in plain Java 21 and use CountDownLatch, CyclicBarrier and Semaphore each where it fits, with runnable checks and the traps I forced.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, java-tutorials, java-collections

## The nightly sync you'll end up with

Here's what the finished job prints when four warehouse feeds load in parallel and the report waits for all of them:

```log
== all loaders count down ==
finished=true rows=3400 remaining=0 (~200 ms)

```

That one line is `CountDownLatch` doing its job. By the end you'll have used all three classes, `CountDownLatch`, `CyclicBarrier` and `Semaphore`, in one small inventory-service job, each for the thing it's actually good at, and you'll know which one to reach for when a colleague asks "how do I make thread A wait for thread B?"

The short version of the choice: a latch is a one-shot "wait until N things are done", a barrier is a reusable "everyone meets here before the next phase", and a semaphore is a "no more than N at once" limit. If you want the memory-model and AQS internals behind them, the concurrency series covers each one on its own: [CountDownLatch](https://ggorantala.dev/java-countdownlatch/?ref=ggorantala.dev), [CyclicBarrier](https://ggorantala.dev/java-cyclicbarrier/?ref=ggorantala.dev) and [Semaphore](https://ggorantala.dev/java-semaphore/?ref=ggorantala.dev); this article is the hands-on recipe. If you haven't set up a thread pool yet, [how to configure a thread pool with ExecutorService](https://ggorantala.dev/how-to-configure-thread-pool-executorservice-java/?ref=ggorantala.dev) covers that part.

## Versions I used

| Tool  | Version I used                                                    | Check it with  |
| ----- | ----------------------------------------------------------------- | -------------- |
| JDK   | OpenJDK 21.0.12.1                                                 | java -version  |
| Build | none, plain javac                                                 | javac -version |
| IDE   | any; IntelliJ IDEA or Eclipse both work (docs-only, not executed) | Help → About   |

You need Java 21 because I used `Thread.ofPlatform()`. No Maven, no Gradle, no container. Everything is five files you can paste into a folder. Total build time for me was about forty minutes, and nearly all of it went into making the failure cases fail the way I wanted.

## Create the project

Make a folder and the package layout:

```bash
mkdir -p stock-sync/src && cd stock-sync

```

```log
stock-sync/
└── src/
    ├── Warehouse.java
    ├── LoadAllLatch.java
    ├── CountPhases.java
    ├── SupplierCalls.java
    └── Checks.java

```

All files use `package com.example.stock;`. In IntelliJ IDEA, use File → New → Project, pick Java with no build system, JDK 21, and mark `src` as the sources root. In Eclipse, File → New → Java Project, set the execution environment to JavaSE-21 and put the files in `src`. Both are written from the vendors' docs; I ran everything from the command line.

## How to use CountDownLatch, CyclicBarrier and Semaphore, step by step

### Step 1: Describe the data

Goal: four warehouses with different load times so the waiting is visible.

`src/Warehouse.java`

```java
package com.example.stock;

import java.util.List;

/** One warehouse's stock feed: a name and how many SKU rows it holds. */
public record Warehouse(String name, int rows, long loadMillis) {
    public static List<Warehouse> samples() {
        return List.of(
                new Warehouse("Rotterdam", 1200, 120),
                new Warehouse("Leeds", 800, 60),
                new Warehouse("Gdansk", 450, 200),
                new Warehouse("Lyon", 950, 90));
    }
}

```

A record is enough here. If records are new to you, see [how to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev).

Checkpoint: `javac -d out src/Warehouse.java` prints nothing.

### Step 2: Wait for every loader with CountDownLatch

Goal: start four loader threads and block the main thread until all have finished.

`src/LoadAllLatch.java`

```java
package com.example.stock;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/** Step 1: CountDownLatch. Wait for every warehouse loader before building the report. */
public class LoadAllLatch {

    public static int loadAll(List<Warehouse> warehouses, boolean failOneWithoutCountDown) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(warehouses.size());
        AtomicInteger total = new AtomicInteger();
        long start = System.nanoTime();

        for (Warehouse w : warehouses) {
            Thread.ofPlatform().name("loader-" + w.name()).start(() -> {
                try {
                    if (failOneWithoutCountDown && w.name().equals("Gdansk")) {
                        throw new IllegalStateException("feed unreachable");
                    }
                    Thread.sleep(w.loadMillis());
                    total.addAndGet(w.rows());
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getName() + " failed: " + e.getMessage());
                } finally {
                    if (!(failOneWithoutCountDown && w.name().equals("Gdansk"))) {
                        done.countDown(); // finally, so a failure can't leave the latch stuck
                    }
                }
            });
        }

        boolean finished = done.await(1, TimeUnit.SECONDS);
        long ms = (System.nanoTime() - start) / 1_000_000;
        System.out.println("finished=" + finished + " rows=" + total.get()
                + " remaining=" + done.getCount() + " (~" + (ms / 100 * 100) + " ms)");
        return total.get();
    }

    public static void main(String[] args) throws Exception {
        System.out.println("== all loaders count down ==");
        loadAll(Warehouse.samples(), false);
        System.out.println("== Gdansk never counts down ==");
        loadAll(Warehouse.samples(), true);
    }
}

```

The latch starts at 4\. Each loader calls `countDown()`, and `await(1, TimeUnit.SECONDS)` returns `true` once the count hits zero or `false` if a second passes first. Two things I'd insist on. Call `countDown()` in a `finally`, otherwise one exception strands everyone. And always use the timed `await`; a bare `await()` on a latch that never reaches zero hangs the job forever. The second run in `main` deliberately skips the countdown for Gdansk (I wrote that flag only to demonstrate this).

Compile and run:

```bash
mkdir -p out && javac -Xlint:all -d out src/*.java
java -cp out com.example.stock.LoadAllLatch

```

```log
== all loaders count down ==
finished=true rows=3400 remaining=0 (~200 ms)
== Gdansk never counts down ==
loader-Gdansk failed: feed unreachable
finished=false rows=2950 remaining=1 (~1000 ms)

```

Checkpoint: the first run takes about 200 ms, the length of the slowest feed, not the sum of all four. The second waits the full second, reports `finished=false`, and `remaining=1` tells you exactly how many parties never showed up. That count is the best debugging aid a latch gives you.

### Step 3: Keep phases in lockstep with CyclicBarrier

Goal: all four warehouses count, then reconcile, then publish, and nobody starts a phase before the others finish the previous one.

`src/CountPhases.java`

```java
package com.example.stock;

import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

/** Step 2: CyclicBarrier. Every warehouse finishes phase N before anyone starts phase N+1. */
public class CountPhases {

    public static void main(String[] args) throws Exception {
        List<Warehouse> ws = Warehouse.samples();
        AtomicInteger phase = new AtomicInteger(1);
        CyclicBarrier barrier = new CyclicBarrier(ws.size(),
                () -> System.out.println("--- phase " + phase.getAndIncrement() + " complete, all " + ws.size() + " arrived ---"));

        for (Warehouse w : ws) {
            Thread.ofPlatform().name(w.name()).start(() -> {
                try {
                    for (String step : new String[]{"count", "reconcile", "publish"}) {
                        Thread.sleep(w.loadMillis() / 4);
                        System.out.println(w.name() + " did " + step);
                        barrier.await(2, TimeUnit.SECONDS);
                    }
                } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
                    System.out.println(w.name() + " stopped: " + e.getClass().getSimpleName());
                }
            });
        }
        Thread.sleep(1500);
        System.out.println("== a party goes missing ==");
        CyclicBarrier b = new CyclicBarrier(3);
        for (int i = 0; i < 2; i++) {
            int id = i;
            Thread.ofPlatform().name("short-" + id).start(() -> {
                try {
                    b.await(300, TimeUnit.MILLISECONDS);
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getName() + " -> " + e.getClass().getSimpleName()
                            + ", broken=" + b.isBroken());
                }
            });
        }
        Thread.sleep(800);
    }
}

```

The barrier is built for 4 parties plus a barrier action that runs once per trip, on the last thread to arrive. Unlike a latch it resets itself after each trip, which is why the same `barrier` object serves all three phases. The second half of `main` builds a barrier for three parties and only sends two, with a 300 ms timeout.

```bash
java -cp out com.example.stock.CountPhases

```

```log
Leeds did count
Rotterdam did count
Lyon did count
Gdansk did count
--- phase 1 complete, all 4 arrived ---
Leeds did reconcile
Lyon did reconcile
Rotterdam did reconcile
Gdansk did reconcile
--- phase 2 complete, all 4 arrived ---
Leeds did publish
Lyon did publish
Rotterdam did publish
Gdansk did publish
--- phase 3 complete, all 4 arrived ---
== a party goes missing ==
short-1 -> BrokenBarrierException, broken=true
short-0 -> TimeoutException, broken=true

```

Checkpoint: no "reconcile" line appears before the phase 1 banner. The order of warehouse names inside a phase changes from run to run; the banners never do. The last two lines are the part that surprised me the first time: only one thread gets the `TimeoutException`, and everyone else gets `BrokenBarrierException`. Once a barrier breaks, it stays broken for every waiter until you call `reset()`, so treat a broken barrier as a failed run, not something to retry in place.

### Step 4: Cap supplier calls with Semaphore

Goal: ten SKUs need a supplier lookup, but the supplier's API allows three concurrent requests.

`src/SupplierCalls.java`

```java
package com.example.stock;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

/** Step 3: Semaphore. At most 3 supplier API calls in flight, out of 10 requested. */
public class SupplierCalls {

    static final AtomicInteger inFlight = new AtomicInteger();
    static final AtomicInteger peak = new AtomicInteger();

    static void callSupplier(int sku, Semaphore permits) throws InterruptedException {
        permits.acquire();
        try {
            int now = inFlight.incrementAndGet();
            peak.accumulateAndGet(now, Math::max);
            Thread.sleep(50); // pretend HTTP call
        } finally {
            inFlight.decrementAndGet();
            permits.release(); // always in finally
        }
    }

    public static int run(int permitCount, int calls) throws Exception {
        inFlight.set(0);
        peak.set(0);
        Semaphore permits = new Semaphore(permitCount);
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < calls; i++) {
            int sku = i;
            ts.add(Thread.ofPlatform().start(() -> {
                try { callSupplier(sku, permits); } catch (InterruptedException ignored) { }
            }));
        }
        for (Thread t : ts) t.join();
        System.out.println("permits=" + permitCount + " calls=" + calls + " peak=" + peak.get()
                + " availableAtEnd=" + permits.availablePermits());
        return peak.get();
    }

    public static void main(String[] args) throws Exception {
        run(3, 10);
        run(1, 5);
        // tryAcquire: shed load instead of queueing
        Semaphore p = new Semaphore(1);
        p.acquire();
        System.out.println("tryAcquire while exhausted = " + p.tryAcquire());
        // the classic leak: release without acquire grows the pool
        Semaphore leak = new Semaphore(2);
        leak.release();
        System.out.println("after stray release, permits = " + leak.availablePermits());
    }
}

```

```bash
java -cp out com.example.stock.SupplierCalls

```

```log
permits=3 calls=10 peak=3 availableAtEnd=3
permits=1 calls=5 peak=1 availableAtEnd=1
tryAcquire while exhausted = false
after stray release, permits = 3

```

Checkpoint: `peak` never exceeds the permit count and `availableAtEnd` equals what you started with. The last line is the semaphore trap: `release()` doesn't check that you ever acquired, so a stray release quietly grows the pool from 2 to 3\. That's why the acquire goes before the `try` and the release goes in the `finally`. If you'd rather shed load than queue, `tryAcquire()` returns `false` immediately, which is what the third line shows.

## Prove it with checks

I didn't have Maven Central access in my environment, so these are plain checks without JUnit; they translate directly to JUnit 5 assertions once you add it. `src/Checks.java`

```java
package com.example.stock;

import java.util.concurrent.*;

public class Checks {
    static int passed, failed;
    static void check(String name, boolean ok) {
        if (ok) passed++; else failed++;
        System.out.println((ok ? "PASS " : "FAIL ") + name);
    }
    public static void main(String[] args) throws Exception {
        check("latch loads every row", LoadAllLatch.loadAll(Warehouse.samples(), false) == 3400);
        check("a missing countDown makes await time out",
                LoadAllLatch.loadAll(Warehouse.samples(), true) == 2950);
        CountDownLatch l = new CountDownLatch(1);
        l.countDown(); l.countDown();
        check("countDown below zero is a no-op", l.getCount() == 0);
        check("semaphore(3) peak <= 3", SupplierCalls.run(3, 10) <= 3);
        check("semaphore(1) peak == 1", SupplierCalls.run(1, 5) == 1);
        Semaphore s = new Semaphore(0);
        check("tryAcquire on empty fails", !s.tryAcquire());
        CyclicBarrier b = new CyclicBarrier(2);
        Thread t = Thread.ofPlatform().start(() -> { try { b.await(); } catch (Exception e) { } });
        b.await(1, TimeUnit.SECONDS);
        t.join();
        check("barrier resets after trip", b.getNumberWaiting() == 0 && !b.isBroken());
        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }
}

```

```bash
java -cp out com.example.stock.Checks

```

```log
PASS latch loads every row
PASS a missing countDown makes await time out
PASS countDown below zero is a no-op
PASS semaphore(3) peak <= 3
PASS semaphore(1) peak == 1
PASS tryAcquire on empty fails
PASS barrier resets after trip
7 passed, 0 failed

```

(The program also prints the job's own log lines between the checks; I've trimmed them.) `javac -Xlint:all` was clean.

## Which one to use

Think of what the count means. In a latch it only goes down and never comes back, so it models an event that happens once: startup finished, all loaders done. A barrier counts arrivals and resets, so it models a recurring meeting point. A semaphore counts free slots that go both up and down, so it models a resource. Mixing them up is the usual mistake: people use a latch in a loop and wonder why the second iteration doesn't wait, because it can't be reset.

All three are built on the same queued-synchronizer machinery in the JDK; the pieces above link to the deep dives, and [CountDownLatch vs CyclicBarrier](https://ggorantala.dev/countdownlatch-vs-cyclicbarrier/?ref=ggorantala.dev) compares the first two side by side. For limiting concurrent calls in a real service, see also [how to implement rate limiting in Java](https://ggorantala.dev/how-to-implement-rate-limiting-java/?ref=ggorantala.dev), and for handing work between threads, [how to use BlockingQueue for producer-consumer pipelines](https://ggorantala.dev/how-to-use-blockingqueue-producer-consumer-java/?ref=ggorantala.dev).

## Errors I hit, and the fixes

- Program hangs and never exits after `await()`. A loader threw before `countDown()`. Put `countDown()` in `finally` and use the timed `await`.
- `java.util.concurrent.BrokenBarrierException`. Another party timed out, was interrupted, or the barrier action threw. Fix the cause; the barrier stays broken until `reset()`.
- `error: cannot find symbol` on `Thread.ofPlatform()`. You're compiling with `--release 17`. I confirmed this: that method needs Java 21\. On 17, use `new Thread(runnable, name).start()`.
- Peak concurrency above the permit count. Check for a `release()` outside the `finally` or a release without a matching acquire.
- Barrier party count doesn't match thread count. With 5 threads on a 4-party barrier, the fifth starts the next trip alone and never finishes. Size the barrier to the number of threads that will call `await()`.

## Before you ship it

- Prefer an `ExecutorService` over raw threads in real code. I used raw threads to keep the steps visible.
- Latches and barriers don't propagate exceptions. If you need results and failures from tasks, [CompletableFuture](https://ggorantala.dev/completablefuture-for-async-programming/?ref=ggorantala.dev) usually fits better. For a "wait for all tasks" join, `invokeAll` on an executor is simpler than a latch.
- On Java 21 with virtual threads, a `Semaphore` is the standard way to limit concurrency, because capping a virtual-thread pool doesn't limit anything.
- I didn't test on Windows; nothing here is OS-specific.

## Checklist

- Use `CountDownLatch` for one-shot waits; `countDown()` in `finally`, `await` with a timeout.
- Use `CyclicBarrier` for repeated phases; size it exactly to the number of waiting threads; a break is final until `reset()`.
- Use `Semaphore` for limits; acquire before `try`, release in `finally`, never release without acquiring.
- Check `getCount()` / `availablePermits()` when something hangs; they tell you who's missing.

## Questions people ask about these three classes

**What is the difference between CountDownLatch and CyclicBarrier in Java?** A latch is single-use and the threads counting down needn't be the ones waiting. A barrier is reusable and the threads that wait are the same threads that arrive.

**Can I reuse a CountDownLatch?** No. Once it reaches zero it stays at zero. Create a new one per round, or use a `CyclicBarrier` or `Phaser`.

**When should I use a Semaphore instead of a thread pool size?** When the limit applies to a resource, such as a remote API or a database, rather than to the number of worker threads.

**Does CountDownLatch work with virtual threads?** Yes, blocking in `await()` parks a virtual thread cheaply on Java 21.

## References

- Java SE 21 API: `CountDownLatch`, `CyclicBarrier`, `Semaphore`, `Thread.Builder` (docs.oracle.com/en/java/javase/21/docs/api)
- JEP 444: Virtual Threads (openjdk.org/jeps/444)