Synchronizers

Java's util.concurrent package ships purpose-built coordination classes — CountDownLatch, CyclicBarrier, Semaphore, Phaser, and Exchanger — for the concurrency patterns raw locks handle only clumsily.

Beyond synchronized blocks and Lock implementations, java.util.concurrent provides several ready-made synchronizer classes, each purpose-built for a specific coordination pattern: waiting for a group of tasks to finish, waiting for a group of threads to all reach a point together, limiting concurrent access to a resource, coordinating dynamically-sized groups of tasks, or handing data directly between two threads. Reaching for one of these instead of hand-rolling equivalent logic with wait/notify is almost always safer and clearer.

Why Synchronizers Exist

Locks answer one question: 'can this thread proceed into a critical section right now?' Many real coordination problems are shaped differently — 'wait until N other things have happened,' 'don't let more than K threads in at once,' 'block until all threads in a group arrive at the same point.' These patterns are common enough that the JDK gives them dedicated, well-tested classes instead of leaving every team to reinvent them on top of wait/notify.

All of these classes live in java.util.concurrent and are built on top of AbstractQueuedSynchronizer (AQS) internally — you do not need to understand AQS to use them correctly.

CountDownLatch — Wait for N Events

A CountDownLatch starts with a fixed count. Threads call countDown() to decrement it; other threads call await() to block until the count reaches zero. It is a one-shot gate — once the count hits zero it cannot be reset or reused.

One latch acts as a starting gun (count 1, released once), another as a finish line (count 3, one decrement per worker).

import java.util.concurrent.CountDownLatch;

CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            startSignal.await();          // wait for the go signal
            System.out.println("worker " + id + " running");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            doneSignal.countDown();        // report completion
        }
    }).start();
}

startSignal.countDown();  // release all workers at once
doneSignal.await();       // main thread waits for all 3 to finish
System.out.println("all workers done");

CyclicBarrier — Rendezvous Point

A CyclicBarrier makes a fixed number of threads all wait at a common point until every one of them has arrived, then releases them all together. Unlike CountDownLatch, it is reusable — once the parties are released the barrier resets automatically for another round. An optional barrier action (a Runnable) runs once, on one of the threads, right when the barrier trips.

The same barrier is used across two phases; each call to await() blocks until all 3 parties arrive at that call.

If any waiting thread is interrupted or times out, the barrier becomes 'broken' and every other waiting thread wakes with a BrokenBarrierException — there is no partial recovery.

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("all arrived, phase complete"));

Runnable worker = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " working phase 1");
        barrier.await();               // wait for the other two
        System.out.println(Thread.currentThread().getName() + " working phase 2");
        barrier.await();               // reused for a second round
    } catch (InterruptedException | BrokenBarrierException e) {
        Thread.currentThread().interrupt();
    }
};
for (int i = 0; i < 3; i++) new Thread(worker).start();

Semaphore — Bounded Access

A Semaphore maintains a set number of permits. acquire() takes a permit (blocking if none are available), and release() returns one. It generalizes a mutual-exclusion lock (a semaphore with 1 permit behaves like a lock) into 'at most K concurrent holders' — the classic use is bounding concurrent access to a limited resource such as a connection pool or a fixed number of worker slots.

Five threads compete, but only two hold a permit at once; the rest block in acquire() until one is released.

A semaphore does not track which thread holds which permit — any thread can call release(), even one that never called acquire(). That flexibility is powerful but means the JVM cannot detect misuse for you.

import java.util.concurrent.Semaphore;

Semaphore pool = new Semaphore(2); // at most 2 concurrent users

Runnable task = () -> {
    try {
        pool.acquire();
        System.out.println(Thread.currentThread().getName() + " using resource");
        Thread.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        pool.release();
    }
};
for (int i = 0; i < 5; i++) new Thread(task).start(); // only 2 run at a time

Phaser — Dynamic Multi-Phase Coordination

Phaser generalizes CyclicBarrier and CountDownLatch: it supports multiple phases like a barrier, but the number of registered parties can change dynamically (parties can register and deregister between phases), which a fixed-size CyclicBarrier cannot do. It is more flexible but also more complex, and is reached for far less often than the other synchronizers.

Parties register and deregister as work is added and completed, something a fixed-count CyclicBarrier cannot express.

import java.util.concurrent.Phaser;

Phaser phaser = new Phaser(1); // 1 party: the main/controlling thread

for (int i = 0; i < 3; i++) {
    phaser.register(); // dynamically add a party
    final int id = i;
    new Thread(() -> {
        System.out.println("task " + id + " phase 1");
        phaser.arriveAndAwaitAdvance(); // wait for all registered parties
        System.out.println("task " + id + " phase 2");
        phaser.arriveAndDeregister();
    }).start();
}
phaser.arriveAndDeregister(); // main thread leaves phase 1's wait

Exchanger — Pairwise Handoff

An Exchanger is a rendezvous point for exactly two threads to swap objects. Each thread calls exchange(myObject), which blocks until the other thread also calls exchange, at which point both receive the other's object and both proceed. It is a narrow tool used for pipeline stages that hand off buffers to each other.

import java.util.concurrent.Exchanger;

Exchanger<String> exchanger = new Exchanger<>();

new Thread(() -> {
    try {
        String received = exchanger.exchange("from producer");
        System.out.println("producer got: " + received);
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

new Thread(() -> {
    try {
        String received = exchanger.exchange("from consumer");
        System.out.println("consumer got: " + received);
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

Choosing the Right Synchronizer

CountDownLatch : one-shot; N events must happen before waiters proceed
CyclicBarrier  : reusable; fixed group of threads sync at a common point
Semaphore      : bound concurrent access to K permits (resource pooling)
Phaser         : multi-phase sync with a dynamically changing party count
Exchanger      : exactly two threads swap data at a rendezvous point

Practical Use Cases

CountDownLatch: a main thread waiting for several parallel initialization tasks (loading config, warming caches, connecting to services) to complete before starting the application. CyclicBarrier: simulations where all worker threads must finish computing one time-step before any of them starts the next. Semaphore: limiting concurrent database connections or API calls to respect a rate or resource limit. Phaser: multi-stage batch pipelines where the number of active workers changes over time. Exchanger: a producer/consumer pair that swaps full and empty buffers instead of copying data through a queue.

Timeouts and Interruption

All of these classes offer timed variants (await(timeout, unit), tryAcquire(timeout, unit), exchange(obj, timeout, unit)) so a thread does not block forever if coordination never completes — always prefer the timed form in production code paths that must remain responsive, and always handle InterruptedException by either propagating it or re-setting the interrupt flag (Thread.currentThread().interrupt()), never by silently swallowing it.

Key Takeaways

CountDownLatch is a one-shot countdown gate; it cannot be reset.

CyclicBarrier makes a fixed group of threads rendezvous, and resets automatically for reuse across phases.

Semaphore generalizes mutual exclusion to K concurrent permits; any thread may release, even one that never acquired.

Phaser supports multi-phase coordination with a dynamically changing set of parties.

Exchanger is a narrow two-thread data handoff point.

Prefer timed acquire/await variants and always handle InterruptedException properly rather than swallowing it.

CyclicBarrier barrier = new CyclicBarrier(3); // ... one thread never calls barrier.await() due to a bug

CyclicBarrier barrier = new CyclicBarrier(3); try { barrier.await(5, TimeUnit.SECONDS); } catch (TimeoutException e) { // handle a stuck party instead of blocking forever }

An untimed await() on a barrier or latch blocks forever if a party never arrives; use timed overloads for resilience.

CountDownLatch latch = new CountDownLatch(3); // ... later, code tries to reuse the same latch for a second round

// Use a new CountDownLatch per round, or use CyclicBarrier if reuse across rounds is needed CountDownLatch latch = new CountDownLatch(3);

CountDownLatch is one-shot; once it reaches zero it stays at zero. Reuse requires a fresh latch or a different synchronizer (CyclicBarrier).

semaphore.acquire(); doWork(); // throws an exception semaphore.release(); // never reached semaphore.acquire(); try { doWork(); } finally { semaphore.release(); }

A permit leaked by a skipped release() permanently shrinks the pool; always release in a finally block.

What is the key difference in reusability between CountDownLatch and CyclicBarrier?

CountDownLatch is one-shot — once its count reaches zero it cannot be reset. CyclicBarrier automatically resets after each group of parties is released, so it can be reused across multiple phases.

Can a thread call Semaphore.release() without ever having called acquire()?

Yes. A Semaphore does not track ownership per thread, so any thread may call release(), which simply increments the available permit count. This flexibility means misuse (extra releases) is not detected automatically.

Why does Phaser exist when CyclicBarrier and CountDownLatch already cover barrier and countdown semantics?

Phaser supports a dynamically changing number of registered parties across multiple phases (parties can register/deregister between phases), which a fixed-party-count CyclicBarrier cannot express.

What happens to threads waiting on a CyclicBarrier if one waiting thread is interrupted?

The barrier becomes 'broken' and all other waiting threads wake up immediately with a BrokenBarrierException, since the barrier can no longer guarantee all parties will arrive.

When would you choose Exchanger over a BlockingQueue for producer/consumer coordination?

Exchanger is suited to exactly two threads directly swapping full buffers for empty ones (double-buffering pipelines) without an intermediate queue; BlockingQueue fits any number of producers/consumers with buffered decoupling.

Why should code always call await()/acquire() with a timeout in production systems rather than the untimed version?

An untimed wait can block a thread indefinitely if another party never arrives (due to a bug, crash, or deadlock elsewhere), making the system unresponsive; a timeout lets code detect and recover from a stuck coordination point.

What guarantee does a Semaphore with 1 permit provide, and how does that relate to a mutual-exclusion lock?

A Semaphore initialized with 1 permit allows only one acquirer at a time, functioning like a basic mutual-exclusion lock — though unlike java.util.concurrent.locks.Lock, it has no reentrancy and no ownership tracking.

Write a program where a main thread starts 4 worker threads, waits for all of them to finish initialization, then prints a message once all are ready.

import java.util.concurrent.CountDownLatch; public class InitDemo { public static void main(String[] args) throws InterruptedException { int workerCount = 4; CountDownLatch ready = new CountDownLatch(workerCount); for (int i = 0; i < workerCount; i++) { final int id = i; new Thread(() -> { System.out.println("worker " + id + " initializing"); ready.countDown(); }).start(); } ready.await(); System.out.println("all workers ready"); } }

Uses a CountDownLatch(4); each worker calls countDown() after initializing, and the main thread's await() unblocks only once all four have counted down, guaranteeing 'all workers ready' prints after every worker's initialization message.

What happens when this code runs on a single thread?

It deadlocks (blocks forever on the second acquire()). A Semaphore with 1 permit has only one permit available; the same thread calling acquire() twice without an intervening release() will block on the second call waiting for a permit that will never appear, so 'reached' is never printed.

Write code using a CyclicBarrier so that 3 threads each print 'step 1', wait for each other, then all print 'step 2'.

import java.util.concurrent.CyclicBarrier; public class BarrierDemo { public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(3); Runnable task = () -> { try { System.out.println(Thread.currentThread().getName() + ": step 1"); barrier.await(); System.out.println(Thread.currentThread().getName() + ": step 2"); } catch (Exception e) { Thread.currentThread().interrupt(); } }; for (int i = 0; i < 3; i++) new Thread(task).start(); } }

All three threads print 'step 1' in some interleaved order, then block at barrier.await() until the third one arrives, after which all three proceed to print 'step 2' — no thread prints 'step 2' before every thread has printed 'step 1'.

Semaphore sem = new Semaphore(1);
sem.acquire();
sem.acquire();
System.out.println("reached");