Java CountDownLatch, CyclicBarrier, and Semaphore
Table of Contents + −
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 workingWorker 2 is workingWorker 3 is workingMain is waiting for workersWorker 1 is doneWorker 2 is doneWorker 3 is doneAll 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 loadingPlayer 2 is loadingPlayer 3 is loadingPlayer 1 is readyPlayer 2 is readyPlayer 3 is readyPlayer 1 starts the roundPlayer 2 starts the roundPlayer 3 starts the roundNo 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 readyPlayer 2 is readyPlayer 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 connectionThread 2 wants a connectionThread 3 wants a connectionThread 4 wants a connectionThread 1 got a connectionThread 2 got a connectionThread 3 got a connectionThread 5 wants a connectionThread 6 wants a connectionThread 1 releasing connectionThread 2 releasing connectionThread 4 got a connectionThread 3 releasing connectionThread 5 got a connectionThread 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 aCyclicBarrier.
// ❌ Wrong: expecting a latch to work for a second roundCountDownLatch 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 roundsCyclicBarrier 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 afinallyblock.
// ❌ Wrong: if the query throws, release() is skipped and a permit leakspool.acquire();runQuery(); // throws -> release never runspool.release();
// ✅ Right: finally guarantees the permit goes backpool.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 andawait()hangs. PutcountDown()in afinallyblock. -
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
finallyblock. Pair everyacquire()with arelease()and make sure a failing task still callscountDown(). Thefinallyblock 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
tryAcquirewith 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 finalfield. 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 ofawait()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()untilcountDown()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()andrelease()for a semaphore, always releasing in afinallyblock. - ✅ 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
What does CountDownLatch.await() do?
Why: await() blocks the calling thread until countDown() has dropped the count to zero, then it returns.
- 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
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
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.