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 gate
  • CyclicBarrier: reusable rendezvous for a fixed group
  • Phaser: multi-phase coordination with dynamic parties
  • Semaphore: permit-based admission control
  • Exchanger: two-party swap
  • CompletableFuture: 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:

  1. Is this one-shot waiting for a fixed batch?
  2. Is this repeated phase alignment for a stable group?
  3. Is party membership dynamic across phases?
  4. Is the real need bounded concurrent access?
  5. Is the flow exactly two-party exchange?
  6. Is the workflow really about async result dependency and composition?

That sequence maps cleanly to:

  1. CountDownLatch
  2. CyclicBarrier
  3. Phaser
  4. Semaphore
  5. Exchanger
  6. CompletableFuture

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, and CompletableFuture each 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