Phaser is the most flexible coordination utility in this part of the JDK.
It is what you reach for when CountDownLatch is too one-shot and CyclicBarrier is too rigid.
Its strengths are:
- repeated phases
- explicit phase numbers
- parties that can register and deregister dynamically
That flexibility makes it powerful, and also easier to misunderstand at first.
Problem Statement
Suppose a multi-stage workflow has several rounds:
- load input
- validate
- transform
- publish
Now add two complications:
- different workers may join late
- some workers may leave after finishing their part
That is where the simpler coordination tools start to strain.
CountDownLatch is one-shot.
CyclicBarrier assumes a stable fixed party count.
Phaser exists for the more dynamic phase-oriented case.
Mental Model
A Phaser tracks:
- the current phase number
- the number of registered parties
- how many of those parties have arrived in the current phase
For each phase:
- parties register
- each party arrives
- when all registered parties have arrived, the phase advances
- the next phase begins
- parties may deregister when they are done
This is like a barrier with a richer lifecycle model.
Core API
Important operations include:
register(): add one new participating partybulkRegister(n): add several partiesarrive(): signal arrival without waitingarriveAndAwaitAdvance(): signal arrival and wait for othersarriveAndDeregister(): signal completion and leave future phasesgetPhase(): inspect the current phase numbergetRegisteredParties(): inspect the party count
The existence of both arrival and registration APIs is the key difference from simpler barriers.
Runnable Example
This example models a three-worker import pipeline with three coordinated phases.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class PhaserDemo {
public static void main(String[] args) throws Exception {
Phaser phaser = new Phaser(1); // main thread registers first
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int workerId = 1; workerId <= 3; workerId++) {
final int id = workerId;
phaser.register();
executor.submit(() -> runWorker(id, phaser));
}
for (int phase = 0; phase < 3; phase++) {
System.out.println("Main waiting for phase " + phase);
phaser.arriveAndAwaitAdvance();
}
phaser.arriveAndDeregister();
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
}
static void runWorker(int workerId, Phaser phaser) {
try {
doPhase(workerId, phaser, "load input");
doPhase(workerId, phaser, "validate records");
doPhase(workerId, phaser, "transform rows");
} finally {
phaser.arriveAndDeregister();
}
}
static void doPhase(int workerId, Phaser phaser, String work) {
try {
System.out.println("Worker " + workerId + " starting: " + work);
TimeUnit.MILLISECONDS.sleep(100L * workerId);
System.out.println("Worker " + workerId + " finished: " + work);
phaser.arriveAndAwaitAdvance();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
Notice what this example demonstrates:
- one coordinator thread can participate too
- each phase has a distinct number
- parties can deregister when their work is done
That is exactly the kind of flexibility CountDownLatch and CyclicBarrier do not provide as directly.
Where Phaser Fits Well
Strong use cases:
- staged batch pipelines
- simulations with multiple rounds and changing participants
- tests coordinating several asynchronous actors over phases
- hierarchical or nested coordination where parties do not all live forever
The best sign that Phaser fits is when you naturally describe the workflow in terms of phases and party membership, not just in terms of “wait until N.”
When It Is Too Much
Phaser is easy to overuse.
Poor fits:
- simple one-time startup waiting
- fixed recurring barriers with a stable party count
- workflows better expressed as futures or queues
If the simpler tool already expresses the design clearly, prefer it.
Phaser earns its complexity only when you really need its lifecycle features.
Common Mistakes
Registering the wrong number of parties
If a thread participates without registering, or registers and never arrives, the phase logic breaks.
Forgetting to deregister
If a party leaves the workflow but stays registered, future phases may block forever waiting for a participant that is gone.
Mixing arrival styles carelessly
Using arrive(), arriveAndAwaitAdvance(), and arriveAndDeregister() in the same workflow is fine, but only when the semantics are deliberate.
Otherwise the phase math becomes hard to reason about.
Testing and Debugging Notes
When diagnosing phaser problems, inspect:
- current phase number
- registered party count
- which actor failed to arrive or deregister
Practical logging helps:
- log before each arrival
- log phase numbers explicitly
- log deregistration points
Compared with simpler primitives, Phaser bugs often come from lifecycle mismatches rather than from the API itself.
Decision Guide
Choose Phaser when:
- coordination happens in named or repeated phases
- participants may join or leave
- you need something more flexible than a fixed barrier
Do not choose it when:
- a single latch is enough
- a fixed recurring barrier is enough
- the coordination is really better modeled as task completion or future composition
Key Takeaways
Phaseris the flexible phase-coordination utility for repeated rounds with dynamic party membership.- It supports registration, arrival, waiting, and deregistration explicitly.
- It is stronger than
CountDownLatchandCyclicBarrierwhen workflow phases and party lifecycle both matter. - Use it only when that extra flexibility is genuinely part of the problem.
Next post: Semaphore in Java Deep Dive
Comments