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.
new CountDownLatch(3): main will wait for three workers to finish their setup.
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.
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]);
}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.
two latches. start (count 1) is the starting gun. done (count 3) is the finish line.
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):
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();
}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:
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));
}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.
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 outand 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:
two threads, two buffers. one fills, one drains, and they trade when both are ready.
exchange waits for a partner, then each walks away with the other's object.which one
| you want to | use |
|---|---|
| wait until N events have happened (once) | CountDownLatch |
| start N threads at the same moment | CountDownLatch(1) as a starting gun |
| make a fixed group wait for each other, repeatedly | CyclicBarrier |
| make a changing group wait for each other | Phaser |
| limit concurrent access to N | Semaphore |
| hand objects between exactly two threads | Exchanger |
| hand objects between many producers and consumers | a BlockingQueue (concurrent collections) |
| wait for results, not just completion | Future / CompletableFuture, or structured concurrency |
- 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.