Phaser is the most flexible barrier in java.util.concurrent, and the least understood. CountDownLatch counts down once and is done; CyclicBarrier reuses a fixed number of parties across rounds. Phaser generalises both: parties can register and deregister dynamically, it advances through numbered phases without limit, and a subclass hook decides when the whole thing terminates. If you have ever hit CyclicBarrier's wall — a fixed party count that cannot change once threads are mid-flight — Phaser is the tool you actually wanted.
The mental model: parties and phases
A Phaser tracks two numbers: how many parties are currently registered, and which phase (round) it is on. Each registered party is expected to call arrive() or arriveAndAwaitAdvance() once per phase. When arrivals equal registered parties, the phase advances: the phase counter increments, arrivals reset to zero, and any waiting threads are released together.
The critical difference from CyclicBarrier is that the party count is not frozen at construction. Call register() to add a party and arriveAndDeregister() to remove one, at any time, even while other threads are parked waiting for the phase to advance. The phaser recomputes the threshold on the fly.
Phaser phaser = new Phaser(1); // register the controlling thread as party 0
for (int i = 0; i < workerCount; i++) {
phaser.register(); // one party per worker, added dynamically
executor.submit(() -> {
doPhaseWork();
phaser.arriveAndAwaitAdvance(); // wait for all at the end of the phase
});
}
phaser.arriveAndDeregister(); // controller drops out; workers proceedThe core operations, precisely
The method names look interchangeable but each has an exact contract, and mixing them up is the usual source of a phaser that deadlocks or advances early:
| Method | Arrives? | Blocks? | Use when |
|---|---|---|---|
arriveAndAwaitAdvance() | yes | yes | the normal barrier call — signal done and wait for the round |
arrive() | yes | no | signal done but keep working; do not wait for peers |
awaitAdvance(int phase) | no | yes | wait for a phase you already arrived at (or a monitor thread) |
arriveAndDeregister() | yes | no | this party is finished for good — leave the phaser |
arriveAndAwaitAdvance() returns the next phase number. awaitAdvance() takes the phase you are waiting to leave and returns immediately if the phaser has already moved past it — which is what makes a non-participating monitor thread safe to write.
Every waiting method is uninterruptible by default. If a parked thread is interrupted, the phaser does not release it and does not throw until it advances; use awaitAdvanceInterruptibly() when a thread must bail out of a stuck round.
onAdvance: the termination hook
A phaser does not stop until it is terminated. Termination is decided by the protected method onAdvance(int phase, int registeredParties), called every time a phase completes, on the thread that triggered the advance. Return true to terminate; false to keep going.
Phaser phaser = new Phaser() {
protected boolean onAdvance(int phase, int parties) {
// stop after 5 rounds, or when everyone has deregistered
return phase >= 4 || parties == 0;
}
};The default implementation returns parties == 0 — a plain phaser terminates the moment its last party deregisters. That is why the idiom registers the controller as party 0 up front: it keeps the phaser alive while workers register, then deregisters to release them. Once terminated, arrive and await calls return immediately with a negative phase number rather than blocking — check isTerminated() or the sign of the returned phase to detect it.
Tiered phasers for scalability
Under heavy contention, thousands of parties arriving on a single phaser hammer one atomic state word. Phaser solves this with tiering: construct child phasers with a parent, and arrivals aggregate up the tree rather than all hitting the root.
Phaser root = new Phaser();
Phaser tierA = new Phaser(root); // child registers itself with root automatically
Phaser tierB = new Phaser(root);
// spread workers across tierA / tierB; they synchronise globally,
// but arrival traffic is split across three state words, not oneA child registers with its parent when its registered-party count goes from zero to one, and deregisters when it drops back to zero. The tree still advances as one logical phaser — a phase completes only when every leaf has its arrivals in — but contention is distributed. Reach for this only when profiling shows the phaser itself is the bottleneck; for a few dozen parties a flat phaser is simpler and faster.
Choosing between Phaser, CyclicBarrier, CountDownLatch
They overlap, but the decision is usually clear:
| Need | Use |
|---|---|
| One-shot 'wait for N events', N fixed | CountDownLatch |
| Fixed set of threads looping through rounds, optional barrier action | CyclicBarrier |
| Party count changes at runtime, or you need a termination hook, or non-blocking arrival | Phaser |
CyclicBarrier's barrier action runs on one arriving thread before release, exactly like onAdvance — but its party count is immutable and it throws BrokenBarrierException if any party times out or is interrupted, breaking the barrier for everyone. Phaser has no 'broken' state: a slow or interrupted party simply hasn't arrived yet. That resilience, plus dynamic registration, is why frameworks that fork variable numbers of subtasks per phase (staged pipelines, iterative simulations) standardise on Phaser.
Practical pitfalls
Forgetting to deregister leaks a party forever. A phaser waits indefinitely for a party that has silently died. Wrap phase work in try/finally so an exception still results in arriveAndDeregister().
Registering inside a phase you are already awaiting can shift the threshold under the other waiters. Register parties before the phase begins, or from a party that has already arrived. The 65,535-party limit is real: parties and phase share a packed 64-bit state word, so a single phaser caps at 65,535 registered parties — another reason tiering exists. Finally, a phaser advance establishes a happens-before edge: everything a party did before arrive() is visible to every other party after the advance, so no extra synchronization is needed to publish phase results.
Phaser when the number of participants changes at runtime or you need a per-round termination decision — the two things CyclicBarrier cannot do. Register the controller as party 0 to keep the phaser alive during setup, always deregister in a finally block, and remember arrivals establish happens-before so phase results are published without extra locking.