Synchronizers: CountDownLatch, CyclicBarrier, Phaser & Semaphore
Coordinate groups of threads with the four purpose-built synchronizers in java.util.concurrent -- waiting for N events, meeting all threads at a shared point, handling a variable number of participants across phases, and capping concurrent access to a limited resource.
Learning objectives
- Use CountDownLatch to make one or more threads wait for a fixed set of events to finish
- Use CyclicBarrier to synchronize a fixed group of threads at repeating phase boundaries
- Explain when Phaser's dynamic party registration is worth its added complexity over CyclicBarrier
- Use Semaphore to cap concurrent access to a limited resource, including cross-thread permit release
- Match each synchronizer to the coordination shape it was built for, instead of forcing one to fit every case
◆ Story
A relay race, a group photo, a factory shift changeover, and a parking garage all involve people-of-a-kind waiting on each other — but not in the same shape. A relay race is asymmetric: the next runner starts only once the current one finishes, a literal countdown to zero. A group photo is symmetric: nobody's picture gets taken until everyone is in frame, and then everyone moves on together, a shared meeting point. A shift changeover has people arriving and leaving throughout the day, not a fixed headcount at any single moment — a flexible, phase-based handoff. A parking garage simply caps how many cars can be inside at once, and any car can leave regardless of which one entered first — a limited pool of interchangeable permits.
Locks answer "who gets exclusive access right now?" These four classes answer a different family of questions entirely: "wait until N things have happened," "make everyone arrive before anyone proceeds," "coordinate phases where participants come and go," and "allow at most N concurrent operations." Pick the right one and the code reads almost like the problem statement it's solving; pick the wrong one and the mismatch shows up as fighting the abstraction the whole way through.
CountDownLatch is initialized with a fixed count N. Every call to countDown() decrements that count by one; await() blocks any calling thread until the count reaches zero. Once it hits zero, it stays there permanently — this is a one-shot gate, not reusable, and a fresh latch is required for a second round. Two shapes come up constantly: a controller thread waiting for N workers to each finish and call countDown() once, or N worker threads all waiting on a single controller's one countDown() call to release them simultaneously (an initial count of 1 used purely as a starting gun).
CyclicBarrier flips the asymmetry: every one of N participating threads calls the same await() method, and all of them block until the last one arrives — then all are released together. Unlike a latch, a CyclicBarrier automatically resets itself after every successful trip, ready to be reused for the next phase of a multi-phase computation. It also supports an optional barrier action — a Runnable that runs exactly once per trip, in whichever thread happens to be the last to arrive, which is a safe place to do phase-boundary bookkeeping like merging partial results, since every other participant is guaranteed to be parked and not touching shared state at that moment.
Phaser, available since Java 7, removes CyclicBarrier's one real limitation: its fixed party count. Threads register() and arriveAndDeregister() as first-class, mutable operations at any point, even mid-computation — letting some participants join late or drop out early, something a CyclicBarrier's fixed constructor argument simply cannot express.
| Synchronizer | Reusable? | Shape |
|---|---|---|
| CountDownLatch | No, one-shot | Asymmetric: one group signals, another waits |
| CyclicBarrier | Yes, auto-resets | Symmetric: all N threads are equal, all must arrive |
| Phaser | Yes, multi-phase | Symmetric, but party count can change between phases |
💻 Code example
package com.crackedlabs.concurrency.synchronizers; import java.util.concurrent.CountDownLatch; public class ServiceStartupGate { public static void main(String[] args) throws Exception { // Exactly three countDown() calls are needed before await() can // return -- this count is fixed at construction and never resets. CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { final int id = i; new Thread(() -> { System.out.println("Service " + id + " initialized."); latch.countDown(); // decrements the shared count by one }).start(); } System.out.println("Main thread: waiting for services..."); latch.await(); // blocks until the count reaches zero System.out.println("All services ready. Startup complete."); } }
A Semaphore generalizes "exactly one thread at a time" to "at most N threads at a time." Picture a parking garage with a fixed number of spaces: a car enters only if a space is free, and any car leaving frees a space for the next one waiting — crucially, the car that leaves doesn't have to be the same car that took that exact space. A semaphore has N permits; acquire() takes one (blocking if none are currently available), and release() returns one. A semaphore with N=1 behaves like a mutex; a semaphore with N=10 allows up to ten concurrent operations.
The single most important difference from a lock: semaphore permits are not owned by whoever acquired them. A different thread than the one that called acquire() can legally call release() — a lock must always be released by the thread that acquired it, but a permit has no such requirement. This unlocks patterns a lock can't express directly, like one thread acquiring a permit to start work and a separate signaling thread releasing it once some external event confirms that work is truly finished.
release() belongs in a finally block with exactly the same discipline as unlock() and lock releases elsewhere in this course — a permit that leaks because an exception was thrown while it was held is gone forever, with no error anywhere to indicate it happened. Every subsequent acquire() call is now competing for one fewer permit than the pool was actually built with, a slow, silent form of resource starvation that's genuinely hard to diagnose after the fact.
A CyclicBarrier can also be reused for a real multi-phase computation this same way: two threads both call barrier.await() at the end of phase one, both proceed together into phase two once the last one arrives, and the same barrier object can be reused for a second rendezvous at the end of phase two — no new object needed, since it auto-resets after every successful trip.
💻 Code example
package com.crackedlabs.concurrency.synchronizers; import java.util.concurrent.Semaphore; public class ConnectionPoolLimiter { public static void main(String[] args) throws Exception { // Only 2 permits exist, so at most 2 threads may be "inside" the // acquire()/release() section at any given moment. Semaphore pool = new Semaphore(2); Runnable task = () -> { try { pool.acquire(); // blocks here if zero permits are available try { System.out.println(Thread.currentThread().getName() + " acquired a connection. Working..."); Thread.sleep(200); // simulated work while holding the permit } finally { System.out.println(Thread.currentThread().getName() + " releasing connection."); pool.release(); // ALWAYS in finally -- never leak a permit } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Thread t1 = new Thread(task, "Worker-1"); Thread t2 = new Thread(task, "Worker-2"); Thread t3 = new Thread(task, "Worker-3"); // more threads than permits t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); } }
▲ Common mistake
A CountDownLatch constructed with a count that doesn't match the actual number of countDown() calls that will happen — say, a worker thread throws an exception before reaching its countDown() line. await() then blocks forever, and the program simply hangs with no error message anywhere. The fix is the same discipline as every resource-release rule in this course: call countDown() in a finally block if there's any chance the preceding code can fail.
The identical failure shape exists for CyclicBarrier: if the party count passed to the constructor doesn't match the number of threads that actually call await(), the barrier never trips, and every thread that did call await() blocks forever — a "phantom" deadlock that looks structurally identical to the miscounted-latch bug, just one class over.
Phaser's flexibility comes with a matching manual-protocol risk: forgetting a single arriveAndDeregister() call anywhere in a worker's lifecycle means the phaser's registered party count never reaches zero, and it never terminates — the tradeoff for dynamic registration is that nothing enforces the bookkeeping the way a fixed constructor argument implicitly does for CyclicBarrier.
A Semaphore permit leaked outside a finally block is arguably the hardest of these bugs to diagnose, because nothing throws or logs when a permit goes missing — the pool simply, permanently, has one fewer permit than it was built with, and the only symptom is gradually worse contention that has no obvious root cause in a thread dump.
A CountDownLatch cannot be reset once it reaches zero — this is by design, not a missing feature, and it's the reason a CyclicBarrier exists as a separate class rather than CountDownLatch simply gaining a reset method. If a coordination point needs to be reused across repeating phases, CyclicBarrier (fixed party count) or Phaser (variable party count) are the correct tools, not a latch recreated by hand each time.
Phaser also supports a tiered, hierarchical mode intended specifically for coordinating very large numbers of parties more efficiently than one flat barrier could — internally spreading the coordination overhead across a tree of sub-phasers instead of one shared structure every party contends on. Most applications never need this tier directly, but it's why Phaser scales to scenarios a flat CyclicBarrier structurally cannot.
▲ Edge case
A binary semaphore (new Semaphore(1)) resembles a mutex but is not one in the strict sense: it has no notion of ownership and is not reentrant. A thread that already holds the single permit and calls acquire() again blocks itself, waiting for a permit only it could release — unlike ReentrantLock, which explicitly tracks hold counts per owning thread to allow exactly that.
The optional barrier action passed to a CyclicBarrier's constructor is worth remembering precisely: it runs exactly once per trip, in whichever thread happens to arrive last, which is by definition the only moment every other participant is guaranteed to be parked and not concurrently touching shared state — making it a genuinely safe, lock-free place to merge results between phases.
Application startup sequences that must wait for several independent subsystems — a database connection pool, a cache warm-up, a config service — to all finish initializing before accepting traffic are a textbook CountDownLatch use case: one controller thread waits, N subsystems each call countDown() once ready.
Parallel batch-download-then-merge workflows (fetching several independent files or API responses, then combining them once all are present) use the same latch shape, or a CyclicBarrier when the "downloaders" themselves need to proceed to a shared next step together rather than just signal a separate waiting controller.
Distributed and batch job frameworks that process data in explicit phases — map, then shuffle, then reduce, for instance — commonly use a barrier-like construct to ensure every worker has finished one phase before any worker starts the next, which is precisely what CyclicBarrier's barrier action and Phaser's phase-advance callback are built to coordinate.
Connection pools and API rate limiters are Semaphore's most direct real-world shape: a fixed number of permits representing available connections or allowed concurrent requests, acquired before use and released afterward — sometimes combined with a scheduled task that periodically adds permits back for time-windowed rate limiting.
ForkJoinPool and recursive divide-and-conquer algorithms use coordination internally that's conceptually close to Phaser's dynamic registration model — sub-tasks that spawn further sub-tasks and need to know when an entire, dynamically-sized tree of work has completed, not just a fixed, upfront-known set of participants.
Q: What's the key structural difference between CountDownLatch and CyclicBarrier?
A: CountDownLatch is asymmetric and one-shot: one group of threads calls countDown() while a different group calls await(), and once the count hits zero it can never be reused. CyclicBarrier is symmetric and reusable: every one of N threads calls the same await(), all block until the last arrives, and the barrier automatically resets for the next round.
Q: When does Phaser's added complexity actually earn its keep over CyclicBarrier?
A: When the number of participants changes between phases -- some threads finishing early and dropping out, others joining partway through -- since CyclicBarrier's party count is fixed for its whole lifetime and cannot express that.
Q: What can a Semaphore do that a lock structurally cannot?
A: A permit acquired by one thread can be released by a completely different thread, since permits have no notion of ownership. A lock must always be released by the exact thread that acquired it.
Q: Why does a miscounted CyclicBarrier party count cause a deadlock with no visible cycle?
A: If the constructor's party count doesn't match the number of threads that actually call await(), the barrier never trips -- every thread that did call await() simply blocks forever, waiting for arrivals that will never come, with no classic two-thread waiting cycle for a thread dump to reveal.
Q: Why must release() on a Semaphore always sit inside a finally block?
A: Because an exception thrown while a permit is held, without a finally-guaranteed release, leaks that permit permanently -- every future acquire() call is now competing for one fewer permit than the pool actually has, with no exception or log message to indicate why.
Want a visual for this concept?
Generate a diagram tailored to “Synchronizers: CountDownLatch, CyclicBarrier, Phaser & Semaphore” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →