DevNotes
Support
Multithreading · Chapter 13

Synchronizers — CountDownLatch, CyclicBarrier, Semaphore & Phaser

Coordinating threads with CountDownLatch, CyclicBarrier, Semaphore, Phaser and Exchanger — how each works, real use cases such as startup checks and rate limiting, and how to choose between them.

◷ 8 min read§ 12 sections? 6 interview questions{ } 8 code examples

#Introduction

java.util.concurrent provides ready-made synchronizers — objects whose only job is to coordinate threads. They replace most hand-written wait/notify code and are frequent interview topics, especially CountDownLatch vs CyclicBarrier and Semaphore.

SynchronizerOne-line summary
CountDownLatchWait until N events have happened — one-shot
CyclicBarrierN threads wait for each other at a common point — reusable
SemaphoreAllow at most N threads into a section at once
PhaserA flexible, reusable barrier with a changing number of parties
ExchangerTwo threads swap objects at a meeting point

#CountDownLatch

A latch is initialized with a count. Threads call countDown() to decrement it; threads calling await() block until it reaches zero. Once zero, it stays open forever — a latch can't be reset.

Java
CountDownLatch ready = new CountDownLatch(3);

ExecutorService pool = Executors.newFixedThreadPool(3);
pool.submit(() -> { checkDatabase();     ready.countDown(); });
pool.submit(() -> { checkCache();        ready.countDown(); });
pool.submit(() -> { checkMessageQueue(); ready.countDown(); });

if (ready.await(30, TimeUnit.SECONDS)) {   // main waits for all three checks
    startAcceptingTraffic();
} else {
    throw new IllegalStateException("Dependencies not ready in time");
}
count = 3
count = 2
count = 1
count = 0 — main thread's await() returns
Java
pool.submit(() -> {
    try {
        checkDatabase();
    } finally {
        ready.countDown();
    }
});

Use Cases

  • Wait for several services or workers to initialize before starting.
  • Wait for N parallel subtasks to finish (an alternative to invokeAll / allOf).
  • A start gate in tests: new CountDownLatch(1) makes many threads begin at exactly the same moment to provoke race conditions.
Java
CountDownLatch startGate = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
    pool.submit(() -> {
        startGate.await();   // all ten wait here...
        hitTheEndpoint();
        return null;
    });
}
startGate.countDown();       // ...and are released together

#CyclicBarrier

A barrier makes a fixed number of threads wait for each other. Each thread calls await(); when the last one arrives, all are released together. It then resets automatically, so it can be used again — hence "cyclic".

Java
int workers = 4;
CyclicBarrier barrier = new CyclicBarrier(workers,
        () -> System.out.println("All chunks done — merging results"));   // barrier action

for (int i = 0; i < workers; i++) {
    int chunk = i;
    pool.submit(() -> {
        for (int round = 1; round <= 3; round++) {
            processChunk(chunk, round);
            barrier.await();        // wait for the other three before the next round
        }
        return null;
    });
}

The optional barrier action runs once per cycle, on the last thread to arrive, before the others are released.

#CountDownLatch vs CyclicBarrier

CountDownLatchCyclicBarrier
Threads wait for events (count-downs)Threads wait for each other
The counting threads don't waitEvery party calls await() and waits
One-shot — can't be resetReusable — resets after each trip
Count is decremented by anyone, any number of timesFixed number of parties
No completion actionOptional barrier action per cycle
Typical use: main waits for N workersTypical use: N workers proceed in lock-step phases

#Semaphore

A semaphore maintains a number of permits. acquire() takes a permit (blocking if none are available); release() returns one. It limits how many threads can use a resource at the same time.

Java
class ReportGenerator {
    private final Semaphore permits = new Semaphore(5);   // at most 5 concurrent reports

    public Report generate(ReportRequest request) throws InterruptedException {
        permits.acquire();
        try {
            return buildExpensiveReport(request);
        } finally {
            permits.release();   // always in finally
        }
    }
}

Fail Fast Instead of Waiting

Java
if (!permits.tryAcquire(200, TimeUnit.MILLISECONDS)) {
    throw new TooManyRequestsException("Report service is busy");   // HTTP 429
}
try {
    return buildExpensiveReport(request);
} finally {
    permits.release();
}

Use Cases

  • Throttling calls to a rate-limited third-party API.
  • Limiting concurrent access to an expensive resource (report generation, file conversion).
  • Implementing a simple resource pool.
  • A binary semaphore (new Semaphore(1)) works like a lock, but any thread can release it — it has no owner and is not reentrant.

Semaphore vs Lock

SemaphoreReentrantLock
N permits — N threads at onceOne owner at a time
No ownership — any thread can releaseOnly the owner can unlock
Not reentrantReentrant
Controls access to a pool of resourcesProtects a critical section

#Phaser

Phaser (Java 7) generalizes CountDownLatch and CyclicBarrier: it's reusable across phases, and parties can register and deregister dynamically.

Java
Phaser phaser = new Phaser(1);   // register the coordinator

for (String file : files) {
    phaser.register();           // one more party
    pool.submit(() -> {
        parse(file);
        phaser.arriveAndAwaitAdvance();    // end of phase 0: wait for all parsers
        validate(file);
        phaser.arriveAndDeregister();      // done with phase 1, and leave
    });
}

phaser.arriveAndAwaitAdvance();      // coordinator waits for phase 0
phaser.arriveAndDeregister();        // coordinator leaves; workers finish phase 1

Use it when the number of participants isn't known up front or changes between phases.

#Exchanger

Two threads meet at an exchange point and swap objects:

Java
Exchanger<List<Record>> exchanger = new Exchanger<>();

// Filler thread: fills a buffer, then swaps it for an empty one
buffer = exchanger.exchange(fullBuffer);

// Drainer thread: writes a full buffer to disk, then swaps it back
buffer = exchanger.exchange(emptyBuffer);

Niche — used for double-buffering pipelines.

#Choosing a Synchronizer

NeedUse
Wait until N tasks or events have completedCountDownLatch (or CompletableFuture.allOf)
Release many threads at the same instantCountDownLatch(1) as a start gate
N threads repeatedly synchronize between phasesCyclicBarrier
Parties change between phasesPhaser
At most N concurrent users of a resourceSemaphore
Two threads trade dataExchanger
Hand work from producers to consumersBlockingQueue (next chapter)

#Interview Questions

Q1What is a CountDownLatch?

A synchronizer initialized with a count; countDown() decrements it and await() blocks until it reaches zero. It's one-shot and can't be reset.

Q2What is the difference between CountDownLatch and CyclicBarrier?

A latch lets threads wait for a number of events; the threads counting down don't wait, and it can't be reused. A barrier makes a fixed number of threads wait for each other, can run a barrier action, and resets for reuse.

Q3What is a Semaphore?

A counter of permits. acquire() takes one (blocking if none remain) and release() returns one, limiting how many threads can access a resource concurrently.

Q4How would you limit concurrent calls to an external API to 10?

Wrap the call in semaphore.acquire() / release() (in finally) with new Semaphore(10), using tryAcquire(timeout) to fail fast — or use a bulkhead (separate bounded pool, or Resilience4j Bulkhead).

Q5Can a CountDownLatch be reused?

No. Once the count reaches zero it stays at zero. Use a CyclicBarrier or Phaser for reusable coordination.

Q6What happens if a thread waiting on a CyclicBarrier is interrupted?

The barrier breaks and all other waiting threads receive BrokenBarrierException; it must be reset() before reuse.

#Common Mistakes

  • Not calling countDown() / release() in finally — a failure hangs other threads or leaks permits.
  • Calling await() without a timeout on a latch that might never reach zero.
  • Releasing a semaphore you never acquired — permits grow beyond the intended limit.
  • Trying to reuse a CountDownLatch.
  • Using a semaphore as a rate limiter — it limits concurrency, not requests per second.

#Best Practices

  • Use these utilities instead of hand-written wait/notify coordination.
  • Always release permits and count down in finally.
  • Use timed await / tryAcquire in production paths.
  • Use a start-gate latch in concurrency tests to maximize interleavings.
  • Prefer CompletableFuture.allOf when you also need the tasks' results.

#Key Takeaways

  • CountDownLatch waits for N events and is one-shot.
  • CyclicBarrier makes N threads wait for each other and is reusable.
  • Semaphore caps concurrent access with permits; it has no owner.
  • Phaser is a flexible, multi-phase barrier with dynamic parties.
  • Release and count down in finally, and prefer timed waits.