The two flavors of this pattern
"Make these threads run in a specific order" shows up in interviews in exactly two shapes, and it pays to name them separately because the fix is slightly different for each:
- Strict ordering — thread A's work must complete before thread B's work starts, which must complete before thread C's starts, and so on. Once the order is established it never repeats (Print in Order:
first() → second() → third(), each called exactly once).
- Strict alternation — two or more threads must take turns repeatedly, in a fixed round-robin pattern, for many iterations (FooBar:
foo, bar, foo, bar, ...; Print Zero Even Odd: zero, odd, zero, even, zero, odd, ...).
Both are really the same underlying question — "how do I make one thread wait for a signal that only another specific thread can send?" — just applied once versus applied in a loop. Every primitive from the earlier topics on this roadmap (locks, semaphores, condition variables) can express this signal; the pattern is choosing the simplest one that matches the shape of the constraint.
Why "just poll a flag" is wrong — and a red flag to an interviewer
The instinctive first attempt is almost always a shared boolean or counter that one thread busy-spins on:
# thread B, trying to wait for thread A
while not a_is_done:
pass # or Thread.sleep(1)
do_second_thing()
This is wrong on two levels, and interviewers listen for whether you catch both:
- Correctness, if the flag isn't synchronized properly. Without a
volatile/atomic read or a memory barrier, a compiler or CPU is entitled to cache a_is_done in a register and never observe the other thread's write — the loop can spin forever in principle, not just in the worst-case-latency sense.
- Even if made correct with
volatile/atomics, busy-spinning is a design smell. It burns a full CPU core doing nothing useful, it doesn't scale past a handful of waiting threads, and — the detail candidates miss most often — inserting Thread.sleep() calls to "fix" ordering issues is not a fix at all. Sleep-based ordering happens to pass on your machine because your thread scheduler is fast and lightly loaded; it is not a correctness argument, and a stress-tested or heavily-loaded grader will flip the interleaving and fail it. Saying this out loud — "sleeping to enforce order doesn't give a correctness guarantee, only a probabilistic one" — is one of the highest-signal sentences you can say in this style of interview.
The fix in both cases is the same: block the waiting thread on a real synchronization primitive so the OS scheduler wakes it exactly when the signal arrives, with zero spinning and a proper happens-before relationship guaranteed by the primitive itself.
Strict ordering: one-shot gates
For "A before B before C," the cleanest tool is one gate per ordering constraint — a binary semaphore initialized to 0 (locked) or a latch, one per boundary you need to enforce:
gate_AB = semaphore(0) # locked until A signals it
gate_BC = semaphore(0) # locked until B signals it
# thread A
run_first()
release(gate_AB) # unlock B
# thread B
acquire(gate_AB) # blocks until A releases it
run_second()
release(gate_BC) # unlock C
# thread C
acquire(gate_BC) # blocks until B releases it
run_third()
Each gate is used exactly once — acquired once, released once — which is precisely what a CountDownLatch/one-shot event is for, and is arguably a clearer expression of intent than a semaphore here (a latch's API doesn't let you accidentally acquire it twice, which a raw semaphore's API does). Either is an acceptable, idiomatic answer; naming both and picking one deliberately is the strong answer.
Note what this buys you over the boolean-flag approach: acquire() on an empty semaphore or await() on an un-counted-down latch blocks the calling thread in the OS scheduler — zero CPU spent, and the runtime guarantees the waiting thread sees everything the signaling thread did before it signaled (the happens-before edge from Topic 7's memory-model material). You get correctness and efficiency from the same call.
Strict alternation: a turn token, handed off on every step
Alternation is the same idea generalized to a loop, and it has two equally standard implementations:
Option 1 — semaphores handed off round-robin. Give each participant its own semaphore, initialize exactly one of them to a permit of 1 (whoever goes first) and the rest to 0, and have every participant's turn end by releasing the next participant's semaphore:
turn_foo = semaphore(1) # foo goes first
turn_bar = semaphore(0)
# thread "foo", repeated n times
acquire(turn_foo)
print("foo")
release(turn_bar)
# thread "bar", repeated n times
acquire(turn_bar)
print("bar")
release(turn_foo)
This scales cleanly past two participants (Print Zero Even Odd is exactly this with three semaphores and a bit of arithmetic to decide whether zero hands off to odd or even next) and needs no explicit shared counter.
Option 2 — a single shared "whose turn is it" variable guarded by a lock and a condition variable. Every participant acquires the lock, checks a loop condition on the shared turn variable, calls wait() if it isn't its turn yet, does its work, updates the variable, and calls notify_all() so everyone re-checks:
turn = "foo" # protected by `lock`, checked/updated only while holding it
# thread "foo", repeated n times
with lock:
while turn != "foo":
wait(lock)
print("foo")
turn = "bar"
notify_all(lock)
This generalizes even more naturally to conditions that aren't simple round-robin — Fizz Buzz Multithreaded is the canonical example: four threads each wait on a predicate over a shared counter (count % 15 == 0, count % 3 == 0 && count % 5 != 0, etc.) rather than a fixed turn order, which is exactly the guarded-block pattern from Topic 5 applied to four roles instead of two.
Both options are correct and idiomatic; semaphores are usually less code for a fixed, known number of participants passing a single token, while a condition variable scales better when the "whose turn" logic is a genuine predicate rather than a round-robin sequence.
Pitfalls and interview gotchas
- Sleeping to fake ordering. Covered above — always call this out explicitly if you see it, even in your own scratch code, and remove it before you'd call a solution done.
- Forgetting
while, using if, around a wait(). A condition variable can wake up without the condition actually being true yet (a "spurious wakeup," or simply because a different waiter's turn just wasn't yours) — the wait must always be inside a loop that re-checks the predicate, never a one-shot if. This is Topic 5's guarded-block rule, and it applies here without exception.
- Reusing a one-shot latch.
CountDownLatch (Java) and similar one-shot primitives cannot be reset and re-armed; if you need the same ordering constraint to repeat (i.e., you've actually got an alternation problem, not a one-time ordering problem), reach for a semaphore or condition variable instead.
- Off-by-one on who starts first. In the semaphore hand-off pattern, exactly one semaphore must start with a permit — get this wrong and every thread deadlocks immediately, waiting on a semaphore nobody will ever release.
- Solving a 3+ role problem with only 2 semaphores by overloading state in a shared counter without protecting the counter itself. The counter update and the semaphore hand-off need to be part of the same atomic step (or the counter needs its own lock) — see Topic 2's critical-section material.
How this differs from a barrier / rendezvous
If your constraint is "one specific thread runs before another," or "these N threads take fixed, repeating turns," you're in this subtopic. If instead your constraint is "these threads, playing different roles with different required counts, must all arrive together and bond as a group before any of them proceeds" (two hydrogens and one oxygen, not "whoever's turn it is") — that's a step up in structure, and it's the next subtopic: multi-party rendezvous.