5. Barriers
A barrier is a counter guarded by a lock: every thread that calls await() decrements the count and parks on a condition variable; the thread that drives the count to zero runs an optional barrier action, then bumps a generation token and wakes everyone — so all parties threads leave together and the barrier is instantly armed for the next round.
That last clause is the whole point of a CyclicBarrier and the reason it is not a CountDownLatch: a latch counts down to zero once and stays open forever, while a barrier resets itself after each trip so the same object coordinates iterative, phased work (simulation ticks, parallel map/merge rounds, multi-stage pipelines).
Tracing the count-down and the trip
Take new CyclicBarrier(3, mergeAction) — three worker threads must finish a phase before any starts the next. The barrier keeps two pieces of state: count (how many still need to arrive, reset to parties each generation) and a generation object that is the identity of the current round. Here is the exact interleaving when W1 arrives first, then W3, then W2:
| Step | Thread | Action | count after | State |
|---|---|---|---|---|
| 1 | W1 | await() → acquire lock, --count (3→2), count≠0 so trip.await() parks W1, lock released | 2 | W1 blocked |
| 2 | W3 | await() → --count (2→1), parks W3 | 1 | W1, W3 blocked |
| 3 | W2 | await() → --count (1→0): W2 is the tripper | 0 | about to trip |
| 4 | W2 | runs mergeAction.run() on W2's stack, while still holding the lock | 0 | merge runs once |
| 5 | W2 | nextGeneration(): trip.signalAll(), install a fresh generation, reset count = parties (0→3) | 3 | barrier re-armed |
| 6 | W1, W3 | wake, re-check generation, return their arrival index (2 and 1) | 3 | all three released |
Two details engineers miss: the barrier action runs on whichever thread happened to arrive last (not a dedicated thread), and it runs before anyone is released — so it is the safe place to merge per-thread partial results. await() returns the thread's arrival index (parties-1 for the first in, 0 for the tripper), which you can use to elect one thread for round-level work.
Java: a reusable barrier across two phases
This version actually exercises the cyclic property — three workers run two rounds, and the barrier action merges per-round partial sums on the tripper thread. Crucially it joins the workers so main does not exit before output prints (the original example's bug).
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
class BarrierDemo {
static final int PARTIES = 3, ROUNDS = 2;
static final AtomicInteger roundSum = new AtomicInteger();
// Barrier action runs ONCE per trip, on the last-arriving thread,
// before any worker is released — the safe place to merge results.
static final CyclicBarrier barrier = new CyclicBarrier(PARTIES, () ->
System.out.println(" [merge] round total = " + roundSum.getAndSet(0)));
static void worker(int id) {
for (int round = 1; round <= ROUNDS; round++) {
roundSum.addAndGet(id); // this thread's partial work
System.out.println("W" + id + " finished round " + round);
try {
int idx = barrier.await(); // park until all 3 arrive
if (idx == 0) System.out.println("W" + id + " was the tripper");
} catch (InterruptedException | BrokenBarrierException e) {
// barrier is now BROKEN for everyone: stop, do not loop again
Thread.currentThread().interrupt();
return;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[PARTIES];
for (int i = 0; i < PARTIES; i++) {
final int id = i + 1;
ts[i] = new Thread(() -> worker(id), "W" + id);
ts[i].start();
}
for (Thread t : ts) t.join(); // <-- the fix: wait for all
System.out.println("all rounds done");
}
}With ids 1,2,3 each round's sum is 6, printed twice, and the barrier is reused without ever being re-created.
Why the naive version is wrong
The original main calls t1.start(); t2.start(); and returns. The two worker threads are non-daemon, so the JVM does technically wait for them here — but the instant you copy that shape into a context with daemon threads, a thread pool, or an early-returning method, main races ahead of the workers and you see truncated or missing output and no merged result. Always join() (or block on a Future/latch) so the launching thread observes completion. The naive snippet also never showed the reset, the action, or the broken state — the parts that make a barrier a barrier.
Go: there is no CyclicBarrier
Go's standard library ships sync.WaitGroup, which is a one-shot barrier (like a latch): one set of goroutines Add/Done, and waiters Wait until the count hits zero — it does not re-arm. To get the cyclic, all-arrive-then-all-leave behavior with a merge step, you build it from a channel and a per-round goroutine, or reuse a WaitGroup per phase:
package main
import (
"fmt"
"sync"
)
const parties, rounds = 3, 2
func main() {
for round := 1; round <= rounds; round++ {
var wg sync.WaitGroup
var mu sync.Mutex
sum := 0
wg.Add(parties)
for id := 1; id <= parties; id++ {
go func(id int) {
defer wg.Done()
mu.Lock()
sum += id // this goroutine's partial work
mu.Unlock()
fmt.Printf("W%d finished round %d\n", id, round)
}(id)
}
wg.Wait() // barrier: block until all 3 arrive
fmt.Printf(" [merge] round total = %d\n", sum) // runs once per round
}
fmt.Println("all rounds done")
}The idiomatic Go pattern fuses the barrier with the merge: instead of one shared mutable object that resets, you create a fresh WaitGroup per round and let the launching goroutine do the merge after Wait() returns — no barrier action, no tripper thread, no broken state to reason about.
Where the two runtimes differ
- What blocks. Java's
await()parks an OS thread on a JVM condition variable (real kernel-level blocking). Go'swg.Wait()parks a goroutine; the Go scheduler (M:N) frees the underlying OS thread to run other goroutines, so thousands of barrier participants cost almost nothing. - Reuse vs recreate. A Java
CyclicBarrieris designed to be reset and reused; reusing async.WaitGroupafterWaitreturns is allowed but fragile, so Go code typically allocates one per phase — cheap because aWaitGroupis a tiny struct. - The merge step. Java runs it on the last arriver inside the barrier's lock; Go runs it on the waiting goroutine after
Wait(), with no implicit lock — you supply your own synchronization for the shared accumulator. - Failure model. Java surfaces a
BrokenBarrierExceptionto propagate cancellation across all parties; Go has no equivalent — you cancel with acontext.Contextand aselectonctx.Done().
Pitfalls
- Wrong party count → permanent hang.
new CyclicBarrier(N)blocks until exactlyNthreads callawait(). LaunchN-1threads (or have one die before reaching the barrier) and the rest park forever. There is no timeout unless you useawait(timeout, unit). - BrokenBarrierException is contagious. If any waiting thread is interrupted, times out, or someone calls
barrier.reset(), the barrier enters the broken state and every thread at that generation gets aBrokenBarrierException(the interrupted one getsInterruptedException). You must not silently swallow it and loop again — the barrier is unusable until reset. The original snippet's emptycatchhides exactly this. - Exception in the barrier action breaks the barrier. If the
Runnableaction throws, the tripper propagates it and the barrier breaks for all waiters — so keep the action fast and exception-safe, and remember it runs while holding the internal lock. - Using a barrier when you wanted a latch (and vice-versa). One-time "wait for N tasks to finish, then proceed" is a
CountDownLatch— it cannot block the finishers. A barrier blocks the participants themselves and is for repeated, symmetric rendezvous. Picking the wrong one leads to either threads that never wait or threads that wait when they shouldn't. - Assuming a dedicated thread runs the action. The action runs on whichever worker arrived last, so it competes for that thread's time and any thread-local state it touches is the tripper's, not a fixed one.
Takeaways
- A
CyclicBarrieris a count + generation behind a lock:await()decrements, the thread hitting zero runs the action and re-arms the barrier, then all parties leave together. - It auto-resets — use it for phased/iterative work; use
CountDownLatchfor one-shot "wait until done" where the finishers should not block. - The barrier action runs once per trip, on the last arriver, before release — the natural place to merge per-thread partial results.
- Always
join()your workers and never swallowBrokenBarrierException; a broken barrier propagates to all parties and must be reset before reuse. Go has no built-in equivalent — compose one fromsync.WaitGroupor channels, one per phase.
Sources: Oracle Java SE API docs for java.util.concurrent.CyclicBarrier and CountDownLatch (including the OpenJDK CyclicBarrier source — the count/Generation/nextGeneration()/breakBarrier() internals traced above); Brian Goetz et al., Java Concurrency in Practice (barriers vs latches, §5.5); the Go sync.WaitGroup documentation and The Go Programming Language (Donovan & Kernighan) on WaitGroup-based rendezvous and context cancellation. Re-authored and deepened for this guide: replaced the generic paragraph and non-joining hello-world with a traced count-down/trip mechanism, a reusable two-phase example, the BrokenBarrierException failure model, and a side-by-side Go version.
🤖 Don't fully get this? Learn it with Claude
Stuck on 5. Barriers? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **5. Barriers** (Concurrency) and want to truly understand it. Explain 5. Barriers from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **5. Barriers** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **5. Barriers** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **5. Barriers** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.