skip to content
everything.concurrency.synchronizers

synchronizers

CountDownLatch, CyclicBarrier, Semaphore, Phaser.

chapter
chapter 09.12
reading time
7 min read

synchronizers are the ready-made coordination objects in java.util.concurrent. a CountDownLatch lets threads wait until a count reaches zero (one-shot). a CyclicBarrier makes a fixed group wait for each other, then resets. a Semaphore limits how many threads do something at once. a Phaser is a reusable barrier whose party count can change. all of them are the AQS state-plus-queue idea with a different rule for "may I pass?".

the picture

four kinds of gate:

  • a latch is a door that opens once a counter hits zero and then stays open. the people waiting behind it aren't the ones counting down.
  • a barrier is a meeting point: nobody leaves until the whole group is there, then everyone leaves together and the meeting point resets for next time.
  • a semaphore is a parking lot with N spaces: take a ticket to get in, hand it back on the way out, wait at the barrier arm when it's full.
  • a phaser is a barrier for a group whose size changes: people can join or drop out between rounds.
waiting in await()
3count
through
main
W1doing setup
W2doing setup
W3doing setup

new CountDownLatch(3): main will wait for three workers to finish their setup.

pick a synchronizer and play the threads. watch who waits (dashed), what number the gate is counting, and when it opens. try calling await() on the latch after it opened, and releasing a semaphore permit while someone's in line.

CountDownLatch: wait for N things

the count only goes down, and await() returns once it's zero. the threads counting down don't block; only the waiters do.

Main.java
import java.util.concurrent.CountDownLatch;
 
void main() throws InterruptedException {
    int[] results = new int[3];
    CountDownLatch done = new CountDownLatch(3);
    for (int i = 0; i < 3; i++) {
        int id = i;
        Thread.ofPlatform().start(() -> {
            results[id] = (id + 1) * 10;   // each worker fills its own slot
            done.countDown();              // "I'm finished"
        });
    }
    done.await();                          // main waits for all three
    IO.println(results[0] + results[1] + results[2]);
}
output, from jdk 25
60

this one needs a real JVM (java.util.concurrent synchronizers), so it doesn't run in the browser. copy it into java Main.java to try it.

results is a plain array, and main still sees all three writes: everything a thread does before countDown() happens-before another thread returns from await(). same guarantee as join, without needing the Thread objects.

the other classic use is backwards: one count, many waiters. a starting gun.

main
W1
W2
W3
sharedstart
done
·
13

two latches. start (count 1) is the starting gun. done (count 3) is the finish line.

step 1/7
a latch of 1 holds every worker at the line until main fires, so they all start at once. a second latch of 3 collects them at the end. this pair is the backbone of most hand-written concurrency tests.

CyclicBarrier: wait for each other, then go again

a fixed number of parties call await(). the last one to arrive runs the optional barrier action, then everyone is released, and the barrier is ready for the next round. perfect for iterative algorithms where each step needs everyone's previous step (simulations, parallel matrix work):

Main.java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
 
void main() throws InterruptedException {
    AtomicInteger round = new AtomicInteger();
    CyclicBarrier barrier = new CyclicBarrier(3,
            () -> IO.println("round " + round.incrementAndGet() + " done"));  // runs once per round
    Runnable worker = () -> {
        try {
            for (int r = 0; r < 2; r++) {
                // ... compute this thread's chunk of round r ...
                barrier.await();                      // wait for the other two
            }
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
        }
    };
    Thread a = Thread.ofPlatform().start(worker);
    Thread b = Thread.ofPlatform().start(worker);
    Thread c = Thread.ofPlatform().start(worker);
    a.join();
    b.join();
    c.join();
}
output, from jdk 25
round 1 done
round 2 done

this one needs a real JVM (java.util.concurrent synchronizers), so it doesn't run in the browser. copy it into java Main.java to try it.

Semaphore: at most N at a time

a semaphore holds permits. acquire() takes one (waiting if there are none), release() puts one back. the classic job is limiting concurrency against something fragile:

Main.java
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
 
void main() throws InterruptedException {
    Semaphore permits = new Semaphore(3);          // at most 3 at a time
    AtomicInteger inside = new AtomicInteger();
    AtomicInteger most = new AtomicInteger();
    Thread[] threads = new Thread[10];
    for (int i = 0; i < 10; i++) {
        threads[i] = Thread.ofPlatform().start(() -> {
            try {
                permits.acquire();
                try {
                    most.accumulateAndGet(inside.incrementAndGet(), Math::max);
                    Thread.sleep(20);              // the "slow database call"
                    inside.decrementAndGet();
                } finally {
                    permits.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
    for (Thread t : threads) t.join();
    IO.println("never more than 3 at once: " + (most.get() <= 3));
}
output, from jdk 25
never more than 3 at once: true

this one needs a real JVM (java.util.concurrent synchronizers), so it doesn't run in the browser. copy it into java Main.java to try it.

with virtual threads this becomes the main way to limit concurrency: you don't pool virtual threads, you start one per task and put a semaphore in front of the scarce thing.

Phaser: a barrier for a changing group

Phaser generalizes both: parties register() and arriveAndDeregister() at any time, and each phase completes when every currently-registered party has arrived. arriveAndAwaitAdvance() is a barrier wait; plain arrive() says "I'm done with this phase" without waiting, latch-style. it's the one to reach for when tasks spawn subtasks that should join the next round, or when workers finish at different times and should stop being waited for.

Phases.java
Phaser phaser = new Phaser(1);                 // the coordinator counts as one party
for (Task t : tasks) {
    phaser.register();                         // one more party for this phase
    Thread.ofVirtual().start(() -> {
        t.stepOne();
        phaser.arriveAndAwaitAdvance();        // everyone finishes step one...
        t.stepTwo();                           // ...before anyone starts step two
        phaser.arriveAndDeregister();          // done for good, stop waiting for me
    });
}
phaser.arriveAndDeregister();                  // the coordinator steps out

and an Exchanger

the odd one out: two threads meet and swap one object each. the classic use is double buffering, where one thread fills a buffer while the other drains the previous one, then they trade:

filler
drainer
sharedfiller holds
drainer holds
·
buffer A: [ ]buffer B: [ ]

two threads, two buffers. one fills, one drains, and they trade when both are ready.

step 1/5
the first thread to call exchange waits for a partner, then each walks away with the other's object.

which one

you want touse
wait until N events have happened (once)CountDownLatch
start N threads at the same momentCountDownLatch(1) as a starting gun
make a fixed group wait for each other, repeatedlyCyclicBarrier
make a changing group wait for each otherPhaser
limit concurrent access to NSemaphore
hand objects between exactly two threadsExchanger
hand objects between many producers and consumersa BlockingQueue (concurrent collections)
wait for results, not just completionFuture / CompletableFuture, or structured concurrency
quick check
a CountDownLatch has reached 0. a new thread calls await(). what happens?
keylines
  • latch: count down to zero, then open forever. waiters and counters are different threads.
  • barrier: a fixed group meets, leaves together, and the barrier resets.
  • semaphore: N permits. acquire in, release out, in finally.
  • phaser: a barrier where parties can join and leave between phases.
  • all of them give happens-before: work done before arriving is visible after passing.