Why a whole new set of primitives?
Topics 3 through 7 gave you the raw material: locks for mutual exclusion, condition variables for waiting on a predicate, atomics for lock-free updates. Latches and barriers are what you get when you package that raw material into a reusable shape for one specific, extremely common problem: a group of threads need to agree on a moment in time before any of them proceeds. You could build this yourself with a mutex and a condition variable every time — and in fact, understanding how to do exactly that is the interview-testable core of this subtopic — but a mature concurrency library gives you two named shapes for it, and knowing which one to reach for (and why the other one is wrong) is a very common senior-level question.
The latch: a one-shot gate
A latch (the canonical example is Java's CountDownLatch) is initialized with a count, N. Any thread can call the decrement operation; once the count reaches zero, every thread currently blocked on the wait operation is released, and every future call to wait returns immediately. This is the detail that trips people up: a latch cannot be reset or reused. Once it opens, it stays open forever. If you need the same coordination again, you construct a brand-new latch.
latch = CountDownLatch(N)
// N worker threads, each running independently:
do_work()
latch.countDown()
// one or more waiter threads:
latch.await() // blocks until count reaches 0, then returns immediately forever after
proceed()
The classic use case is startup sequencing: don't start accepting traffic until the database pool, the cache client, and the message bus have all finished initializing.
ready = CountDownLatch(3)
spawn { initDb(); ready.countDown() }
spawn { initCache(); ready.countDown() }
spawn { initQueue(); ready.countDown() }
ready.await()
startAcceptingTraffic()
Notice the asymmetry: the three initialization threads call countDown(), but they never call await() — and the main thread calls await() but never countDown(). The threads doing the counting and the threads doing the waiting don't have to be the same set, or even the same size. That asymmetry is one of the two facts that distinguish a latch from a barrier.
The barrier: a reusable rendezvous point
A barrier (the canonical example is Java's CyclicBarrier) is initialized with a number of parties, N. Every one of those N parties calls the same wait operation. The barrier only releases anyone once all N have called it — and critically, it then resets itself automatically for the next round. This is why it's called "cyclic": the same barrier object can synchronize the same N threads at the end of round 1, then again at the end of round 2, and so on, indefinitely.
barrier = CyclicBarrier(N, onBarrierTripped) // action is optional
// each of the N worker threads:
for round in rounds:
computeRound(round)
barrier.await() // blocks until all N workers reach this line
The use case that matters most for interviews — because it's a direct preview of topic 11 (Parallel Algorithm Patterns) — is synchronizing N worker threads at the end of every iteration of a parallel computation: a parallel matrix multiply, an iterative simulation, a distributed gradient-descent step where every worker must finish its slice of the batch before anyone starts the next one. Many barrier implementations also accept an optional action (a Runnable in Java) that runs exactly once per round, executed by whichever thread happens to be the last to arrive, before anyone is released — the natural place to merge each worker's partial result into the shared state for the next round.
The other structural difference from a latch, beyond reusability: in a barrier, the waiters and the "counters" are the same set of threads. There's no separate coordinator thread standing outside the group — every party both contributes to tripping the barrier and is released by it. That symmetry is what "rendezvous" means here: everyone waits for everyone.
The one distinction interviewers actually want to hear
| Latch | Barrier |
|---|
| Reusable? | No — one-shot, cannot reset | Yes — resets automatically each cycle |
| Who's counted vs. who waits | Can be different sets/sizes | Always the same N parties |
| What it models | "Wait for N events" | "Wait for N threads to rendezvous" |
| Typical use | Startup gating, fixed batch completion | Phased/iterative parallel computation |
| Failure behavior | No built-in propagation — a stuck task just hangs await() forever | All-or-none: one broken/timed-out/interrupted party breaks the barrier for everyone waiting |
If an interviewer asks "how would you make a CountDownLatch reusable," the correct answer isn't a trick — it's "you wouldn't; that's precisely what a CyclicBarrier (or the more general Phaser, for a variable party count) is for."
Building a barrier from scratch — the harder ask
A common follow-up: implement a barrier using only a mutex and a condition variable (topic 5's primitives). The standard trick is a generation counter, which exists to solve a subtle bug: without it, a thread that arrives late for round 2 could be mistaken for having satisfied round 1's wakeup, or a spurious wakeup could let a thread "leak" into the wrong round.
class Barrier:
mutex, condition
parties = N
count = N
generation = 0
def await():
with mutex:
gen = generation # snapshot which round I belong to
count -= 1
if count == 0:
# last one in: run the barrier action, then start a new round
runBarrierAction()
count = parties
generation += 1
condition.notifyAll()
else:
condition.wait_for(lambda: generation != gen)
Every waiter captures its own generation before waiting, and only wakes up once generation has actually advanced — so a notifyAll() from a later round can never be mistaken for the round a given thread is actually blocked on. This is exactly the "guarded wait with a predicate, not a bare wakeup" discipline from topic 5, applied to a group instead of a single flag.
Pitfalls and interview gotchas
- Trying to "reset" a
CountDownLatch. There is no such API, and that's intentional — reach for a barrier or Phaser instead.
- Off-by-one on the party count. If a barrier is constructed for N parties but only N-1 threads ever call
await() (one crashed, or a countDown()/await() call was accidentally skipped in an error path), every other thread blocks forever. Always release the latch/barrier side in a finally block.
- Underestimating the barrier's failure blast radius. A
CyclicBarrier's all-or-none breakage (one party timing out or getting interrupted poisons the round for every other waiter) is a feature for tightly-coupled phased algorithms, but a liability if your "parties" are actually independent, fault-tolerant tasks — that's a signal you want per-task fault isolation instead, which is exactly what futures/promises (next subtopic) give you.
- Confusing "wait for completion" with "wait for rendezvous." If you catch yourself asking "does the waiting thread also need to call the same completion signal," you've drifted from latch territory into barrier territory, or vice versa.
Where this leads
Latches and barriers answer "when." They don't hand you anything back — await() returns void. The moment you need the result of the work you waited for, not just the fact that it finished, you need the next subtopic: futures and promises.