By this point in the module, the important thing is not memorizing six APIs.
The important thing is recognizing the coordination shape in front of you.
Most wrong choices happen because a developer notices only that:
- some thread has to wait
and misses the more important question:
- what kind of waiting is this
That question determines the right primitive.
The First Question to Ask
Start here:
- are you coordinating threads, phases, permits, pairwise handoff, or results
That one sentence already narrows the toolset sharply.
Different utilities model different shapes:
CountDownLatch: one-shot gateCyclicBarrier: reusable rendezvous for a fixed groupPhaser: multi-phase coordination with dynamic partiesSemaphore: permit-based admission controlExchanger: two-party swapCompletableFuture: result and continuation coordination
Quick Mental Model Map
Use CountDownLatch for:
- “wait until these N things finish once”
Use CyclicBarrier for:
- “all workers must meet here every round”
Use Phaser for:
- “we have phases, and parties may join or leave”
Use Semaphore for:
- “only up to N of these may run at the same time”
Use Exchanger for:
- “two threads should swap objects at a rendezvous”
Use CompletableFuture for:
- “when this result completes, trigger or combine downstream work”
If you choose from this level first, the API details become much easier.
Scenario-Based Guide
Service startup waiting for warm-up tasks
Use CountDownLatch.
Why:
- fixed task count
- one-time release
- no repeated phases
Repeated simulation or processing rounds
Use CyclicBarrier.
Why:
- same group
- repeated rendezvous
- automatic reset after each round
Multi-stage workflow with changing participants
Use Phaser.
Why:
- parties may join or leave
- phases matter explicitly
Protecting a downstream dependency from too many concurrent calls
Use Semaphore.
Why:
- bounded capacity
- admission control
- concurrency limit
Two threads swapping reusable buffers
Use Exchanger.
Why:
- exactly two parties
- symmetric handoff
- ownership transfer
Fan-out to several async tasks, then combine results
Use CompletableFuture.
Why:
- result dependency graph
- natural fan-in
- richer failure handling
What These Utilities Do Not Replace
This is just as important.
Coordination utilities do not eliminate the need for:
- locks for shared-state invariants
- atomics for single-variable transitions
- queues for decoupled producer-consumer pipelines
If the real bug is unsafe shared mutation, adding a latch or barrier will not fix it. You still need the right state-protection primitive.
Common Wrong Choices
Using CountDownLatch for recurring rounds
This usually means the workflow wants CyclicBarrier or Phaser.
Using Semaphore when the real issue is state ownership
Permits control admission. They do not automatically make compound shared-state updates safe.
Using Exchanger for queue-like workloads
If producers and consumers are not a stable pair, you probably want a queue.
Using CompletableFuture for everything
It is powerful, but not every coordination problem is a completion graph. Sometimes a latch or semaphore is clearer and safer.
A Practical Selection Order
When choosing, walk through this order:
- Is this one-shot waiting for a fixed batch?
- Is this repeated phase alignment for a stable group?
- Is party membership dynamic across phases?
- Is the real need bounded concurrent access?
- Is the flow exactly two-party exchange?
- Is the workflow really about async result dependency and composition?
That sequence maps cleanly to:
CountDownLatchCyclicBarrierPhaserSemaphoreExchangerCompletableFuture
If none of those questions really match, you may be looking at the wrong primitive family entirely.
Testing and Operational Guidance
No matter which utility you choose:
- use timeouts on fragile boundaries
- log phase or acquisition points clearly
- test failure paths, not only the happy path
- keep the coordination boundary narrow and explicit
The hardest coordination bugs usually come from a mismatch between the primitive and the workflow shape, not from forgetting a method name.
Example Shapes
A selector post benefits from more than one concrete example because the whole point is to recognize workflow shape.
Example 1: One-Shot Startup Gate with CountDownLatch
CountDownLatch startup = new CountDownLatch(3);
executor.submit(() -> warmCache(startup));
executor.submit(() -> loadRoutes(startup));
executor.submit(() -> startConsumers(startup));
startup.await();
This is a one-shot gate:
- fixed count
- one release moment
- no reuse across rounds
Example 2: Concurrency Limit with Semaphore
Semaphore dbPermits = new Semaphore(20);
if (dbPermits.tryAcquire()) {
try {
callDatabase();
} finally {
dbPermits.release();
}
}
This is not about phases or completion groups. It is about admission control. Seeing both examples side by side makes the utility boundary much clearer.
Key Takeaways
- Choose coordination utilities by synchronization shape, not by API familiarity.
CountDownLatch,CyclicBarrier,Phaser,Semaphore,Exchanger, andCompletableFutureeach model a distinct kind of waiting or progress.- If the primitive does not match the workflow shape, the code becomes awkward long before it becomes correct.
- The best concurrency design usually starts by naming the exact coordination contract in plain language first.
Next post: Why Ordinary Collections Are Unsafe Under Concurrent Mutation
Comments