Java CountDownLatch, CyclicBarrier, and Semaphore

In the last lesson you learned about Java BlockingQueue and producer-consumer. A blocking queue is great when threads hand work to each other. But sometimes threads do not pass data around. They just need to coordinate. Java gives you three small tools for this, called synchronizers: CountDownLatch, CyclicBarrier, and Semaphore.

🤔 The problem: timing between threads is hard to get right

Three common timing problems show up again and again:

  • Five worker threads load five parts of a screen. You want “all done” only after every part finishes.
  • Three players join a game round. The round must not start until all three have loaded.
  • A database allows only three connections. Twenty threads want in. Let too many rush in and the database crashes.

Hand-writing this with raw wait() and notify() is easy to get wrong. Forget a notify or check the wrong condition and threads freeze forever. The three synchronizers below solve each problem cleanly, with no busy-waiting and no hand-rolled signaling.

⏳ CountDownLatch: wait until N tasks finish

A CountDownLatch is a one-time gate that opens when a counter reaches zero:

  • You create it with a starting count.
  • Each finishing task calls countDown() to drop the count by one.
  • Any thread that calls await() waits until the count hits zero, then passes through.
  • Once it opens, it stays open. It cannot be reset.

Think of it like a checklist with five boxes. The boss waits at the door. Each worker ticks one box as they finish. When the last box is ticked, the boss walks through.

The example below has a main thread wait for three workers. Main calls await(), each worker calls countDown() when done:

import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
int workers = 3;
CountDownLatch latch = new CountDownLatch(workers); // count starts at 3
for (int i = 1; i <= workers; i++) {
final int id = i;
new Thread(() -> {
System.out.println("Worker " + id + " is working");
try {
Thread.sleep(id * 100L); // pretend the job takes a while
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Worker " + id + " is done");
latch.countDown(); // drop the count by one
}).start();
}
System.out.println("Main is waiting for workers");
latch.await(); // blocks until count reaches 0
System.out.println("All workers done. Main continues.");
}
}

The latch starts at three. Main prints that it is waiting, then await() blocks. Each worker calls countDown(). After the third one the count is zero, the latch opens, and await() returns. Worker order shifts between runs, but main always prints last:

Output

Worker 1 is working
Worker 2 is working
Worker 3 is working
Main is waiting for workers
Worker 1 is done
Worker 2 is done
Worker 3 is done
All workers done. Main continues.

The key point: await() does not poll or spin. The thread truly sleeps until the count reaches zero, costing almost nothing.

Put countDown in a finally block

A worker might throw an exception before it reaches countDown(). If that happens, the count never reaches zero and await() waits forever. Put latch.countDown() in a finally block so it runs even when the job fails. That way the gate always opens.

A latch also works the other way. Start several threads that all await() on a latch of one, then call countDown() once to release them together. A handy way to make many threads start at the same instant.

🚧 CyclicBarrier: threads wait for each other

A CyclicBarrier is a meeting point:

  • You tell it how many threads to expect.
  • Each thread works, then calls await() at the barrier and waits there.
  • When the last expected thread arrives, the barrier releases all of them at once.
  • The word cyclic means it resets after it trips, so you reuse it round after round. A latch is one-time; a barrier is reusable.

Picture three friends meeting before a hike. Nobody starts walking until all three reach the trailhead. The first two wait, and when the third shows up they all set off together. Next weekend they do it again at the same spot.

The example below has three threads each “load” a part of a game, then wait at the barrier. Only when all three arrive does the round begin:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
int players = 3;
CyclicBarrier barrier = new CyclicBarrier(players); // wait for 3
for (int i = 1; i <= players; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("Player " + id + " is loading");
Thread.sleep(id * 100L);
System.out.println("Player " + id + " is ready");
barrier.await(); // wait for the others
System.out.println("Player " + id + " starts the round");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}

Each player loads at a different speed, so they reach await() at different times. The first two wait. When the third arrives, the barrier is full and releases all three at once. Only then does any player print “starts the round”:

Output

Player 1 is loading
Player 2 is loading
Player 3 is loading
Player 1 is ready
Player 2 is ready
Player 3 is ready
Player 1 starts the round
Player 2 starts the round
Player 3 starts the round

No player starts until every player is ready. That is the whole job of a barrier.

A barrier action that runs when everyone arrives

A CyclicBarrier can take an optional second argument, a Runnable called the barrier action:

  • It runs once, on the last arriving thread, right before the others are released.
  • It is the perfect spot to combine results or announce the round.

The example below adds a barrier action that prints when all players are ready:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
int players = 3;
CyclicBarrier barrier = new CyclicBarrier(players,
() -> System.out.println(">>> All players ready. Round start! <<<"));
for (int i = 1; i <= players; i++) {
final int id = i;
new Thread(() -> {
try {
Thread.sleep(id * 100L);
System.out.println("Player " + id + " is ready");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}

The barrier action runs exactly once, after the last player arrives and before anyone moves on. So the announcement lands after every “ready” line:

Output

Player 1 is ready
Player 2 is ready
Player 3 is ready
>>> All players ready. Round start! <<<

This is great for “wait for everyone, then do one combined step”, like merging partial results before the next phase.

🎟️ Semaphore: limit how many threads access a resource

A Semaphore hands out a fixed number of permits:

  • A thread calls acquire() to take a permit before using a resource.
  • It calls release() to give the permit back when done.
  • If no permits are left, acquire() waits until another thread releases one.
  • So the semaphore caps how many threads use the resource at once.

Think of a parking lot with three spaces and three tickets. To park, you take a ticket. When all three are gone, the next driver waits at the gate until someone leaves and returns one. The lot never holds more than three cars.

A classic use is a connection pool. The example below gives a semaphore three permits so only three threads query at once:

import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) {
Semaphore pool = new Semaphore(3); // 3 permits = 3 connections
for (int i = 1; i <= 6; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("Thread " + id + " wants a connection");
pool.acquire(); // take a permit, or wait
System.out.println("Thread " + id + " got a connection");
Thread.sleep(200); // pretend to run a query
System.out.println("Thread " + id + " releasing connection");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
pool.release(); // always give the permit back
}
}).start();
}
}
}

Six threads want a connection, but only three permits exist. The first three to call acquire() get permits and start querying. The other three block until a permit frees. Each finishing thread calls release() in finally, waking a waiting thread. So never more than three hold a connection at once:

Output

Thread 1 wants a connection
Thread 2 wants a connection
Thread 3 wants a connection
Thread 4 wants a connection
Thread 1 got a connection
Thread 2 got a connection
Thread 3 got a connection
Thread 5 wants a connection
Thread 6 wants a connection
Thread 1 releasing connection
Thread 2 releasing connection
Thread 4 got a connection
Thread 3 releasing connection
Thread 5 got a connection
Thread 6 got a connection
...

Only three “got a connection” lines appear before a “releasing” line. The semaphore enforces that limit. The release() sits in finally, so a permit always goes back even if the query throws.

A permit is not tied to a thread

Unlike a lock, a semaphore does not remember who took which permit. Any thread can call release(), even one that never called acquire(). That flexibility is powerful, but it means you must pair acquire and release carefully yourself. Releasing too many times silently raises the permit count and breaks the limit.

⚖️ When to use each one

All three coordinate threads, but they answer different questions. This table helps you pick fast:

Tool What it does Reusable? Use it when
CountDownLatch One or more threads wait until a count reaches zero No, one-time Wait until N tasks finish, then continue
CyclicBarrier N threads wait for each other at a meeting point Yes, resets each round Threads must start a phase together, round after round
Semaphore Limits how many threads use a resource at once Yes, permits return Cap concurrent access, like a connection pool

A simple way to remember it. A latch answers “are they all done yet?” A barrier answers “is everyone here yet?” A semaphore answers “is there room for one more?”

⚠️ Common Mistakes

A few traps catch people often.

  • Reusing a CountDownLatch. A latch is one-time. Once the count hits zero it stays there forever, with no reset. A later await() returns instantly because the gate is already open. Need a reusable meeting point? Use a CyclicBarrier.
// ❌ Wrong: expecting a latch to work for a second round
CountDownLatch latch = new CountDownLatch(3);
// ... round 1 counts it down to 0 ...
latch.await(); // round 2: returns immediately, no waiting happens
// ✅ Right: use a CyclicBarrier for repeated rounds
CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // resets after each trip, ready for the next round
  • Forgetting release() on a Semaphore. A permit that is never given back is gone for good. Lose enough and every future acquire() waits forever. Always release in a finally block.
// ❌ Wrong: if the query throws, release() is skipped and a permit leaks
pool.acquire();
runQuery(); // throws -> release never runs
pool.release();
// ✅ Right: finally guarantees the permit goes back
pool.acquire();
try {
runQuery();
} finally {
pool.release();
}
  • Forgetting countDown() on a worker that fails. Same idea as a leaked permit. If a worker throws before countDown(), the count never reaches zero and await() hangs. Put countDown() in a finally block.

  • Mismatching the barrier count. A barrier set for three needs exactly three await() calls to trip. If only two ever call it, both wait forever. Match your thread count to the number you gave the barrier.

  • Catching BrokenBarrierException and ignoring it. If one thread at a barrier is interrupted or times out, the barrier “breaks” and the others get a BrokenBarrierException. Swallowing it hides a real failure. At least log it.

✅ Best Practices

Habits that keep these synchronizers safe and clear.

  • Release permits and count down in a finally block. Pair every acquire() with a release() and make sure a failing task still calls countDown(). The finally block guarantees it.
  • Pick the tool that matches the question. “All done?” is a latch. “Everyone here?” is a barrier. “Room for one more?” is a semaphore. Choosing right makes the code read clearly.
  • Use a barrier action for the combine step. When threads finish a phase together, do the merge or announcement in the barrier action so it runs exactly once.
  • Prefer tryAcquire with a timeout for semaphores when you cannot wait forever. It returns false instead of blocking, so a busy resource does not freeze your thread.
  • Store the synchronizer as a private final field. One shared object that the whole group uses keeps the coordination in one place.
  • Match counts carefully. A latch count equals the number of countDown() calls. A barrier count equals the number of await() callers. Off-by-one here causes hangs.
  • Remember a latch is one-time. If you find yourself wanting to reset a latch, you almost certainly want a CyclicBarrier.

🧩 What You’ve Learned

Nicely done. Let’s recap the three synchronizers.

  • ✅ A CountDownLatch is a one-time gate. Threads await() until countDown() drops the count to zero.
  • ✅ A latch is perfect for waiting until N tasks finish, like a main thread waiting for workers.
  • ✅ A CyclicBarrier makes N threads wait for each other at a meeting point, then releases them together.
  • ✅ A barrier is reusable round after round, and it can run an optional barrier action when everyone arrives.
  • ✅ A Semaphore hands out a fixed number of permits to limit concurrent access, like a connection pool.
  • ✅ Use acquire() and release() for a semaphore, always releasing in a finally block.
  • ✅ Pick the tool by the question: all done (latch), everyone here (barrier), room for one more (semaphore).

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does CountDownLatch.await() do?

    Why: await() blocks the calling thread until countDown() has dropped the count to zero, then it returns.

  2. 2

    What is the key difference between a CountDownLatch and a CyclicBarrier?

    Why: A CyclicBarrier resets after it trips so you can reuse it each round, while a CountDownLatch is one-time and cannot be reset.

  3. 3

    What does a Semaphore control?

    Why: A semaphore hands out a fixed number of permits, so it caps how many threads use a resource concurrently.

  4. 4

    Why should release() on a Semaphore go in a finally block?

    Why: If the work throws before release(), the permit leaks. A finally block guarantees the permit always goes back.

🚀 What’s Next?

These synchronizers coordinate threads you create yourself. But some big jobs split into smaller pieces that each split again, like adding up a huge array by halving it over and over. For that kind of divide-and-conquer work, Java has a special pool that splits tasks and shares them across threads automatically. Next you will learn the Fork/Join framework and how it speeds up recursive work. Let’s learn it.

Java Fork/Join Framework

Share & Connect