Coordinated Conditional Output
Four threads, Number, Fizz, Buzz, FizzBuzz, coordinate to print FizzBuzz for 1..n. The right thread is whichever matches the current number's divisibility. Pattern: shared counter + per-thread Semaphore, dispatcher wakes only the right thread.
The setup
This is the classic FizzBuzz exercise, but multithreaded. It shows up on LeetCode as "Fizz Buzz Multithreaded". The single-threaded version is a beginner exercise: count from 1 to n, and for each number print one of four things based on what divides it:
- If the number is a multiple of 15, print
"fizzbuzz". - Otherwise, if it is a multiple of 3, print
"fizz". - Otherwise, if it is a multiple of 5, print
"buzz". - Otherwise, print the number itself.
So for n = 15 the output is: 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz.
The multithreaded twist: there are now four separate threads, each one in charge of exactly one of the four cases. They are running at the same time. The job is to make sure that for every number from 1 to n, only the right thread prints, the others stay quiet, and the output still comes out in the correct order.
| Thread | Prints | When it fires |
|---|---|---|
| Number | the number itself | i is not divisible by 3 or 5 |
| Fizz | "fizz" | i is divisible by 3 but not 5 |
| Buzz | "buzz" | i is divisible by 5 but not 3 |
| FizzBuzz | "fizzbuzz" | i is divisible by 15 |
A picture of one round
Imagine a small dispatcher in the middle of the room. For each number from 1 to n, the dispatcher looks at the number, decides which of the four threads should handle it, and gives only that thread the green light. The other three stay asleep for this round.
The dispatcher only wakes one thread per round. That thread does its print, signals back "done", and the dispatcher moves on to the next number.
Two ways to write this
The same idea has two common shapes in code, and which one you reach for depends on how the problem is framed.
Shape 1: a dispatcher with one signal per thread
This is what the diagram above shows. Set up four signals, one per thread. The dispatcher loops from 1 to n and uses an if/else chain on the divisibility rules to decide which signal to fire.
Each worker thread sits in a loop: wait on its own signal, print, signal back to the dispatcher that it is done. The dispatcher waits for "done" before moving to the next number, so the output stays in order.
This is what the Java semaphore code and the Go channel code above show. It is fast and easy to reason about because the routing logic lives in one place.
Shape 2: a single shared signal, each thread checks its own rule
A different way: skip the dispatcher. Have one shared counter and one shared signal. Every time the counter changes, the signal is broadcast to all four threads. Each thread wakes up, checks "is this number mine?", and either prints or goes back to sleep.
This is what the Python condition-variable code shows. It scales better when there are many rules (you do not have to hard-code an if/else chain in a dispatcher), but it wastes some work because every thread wakes up on every number.
How to choose between them The way the interviewer describes the problem is a hint:
- "Four threads, four roles, take turns" → use the dispatcher shape. Cleaner, faster.
- "Many threads, each one waits for a condition to be true" → use the broadcast shape. Generalizes better when the number of rules is open-ended.
Either is a fine answer. The senior move is to mention you know both and pick one with a reason.
Why the broadcast version needs a while, not an if
In the broadcast version every thread wakes up every round. So when a thread wakes, it has to ask itself "is the current number actually mine, or did the dispatcher just bump it for someone else?". That question has to be inside a while loop, not an if. Otherwise the thread might print on a wake-up that was meant for somebody else.
This is why the Python code reads while not (predicate): cv.wait(). The thread wakes, checks, and if the answer is "not me", goes back to sleep.
Where this pattern shows up in real systems
The shape of the problem ("look at this thing, decide which handler should run, wake only that handler") is everywhere:
- Message routing in Kafka, RabbitMQ, or any message broker. A message of type X gets delivered only to consumers subscribed to type X.
- Event dispatchers in GUI frameworks. A click event goes to the click handler; a key event goes to the key handler.
- In-process pub/sub. One publisher, many subscribers, each interested in a different topic.
- Workflow orchestrators. A task tagged "image-processing" goes to the image worker pool, not to the email worker pool.
Once you have written the dispatcher shape for FizzBuzz, all of these are the same code with different routing rules.
The shutdown story most candidates forget
When the loop reaches i > n, the dispatcher exits. The four worker threads, however, are still sitting in their wait calls. They will block forever and leak.
Two clean ways to close them out:
- Send a stop signal. After the main loop, fire each thread's signal once more with a value or flag that means "stop". Each worker checks for it and returns.
- Close the channel (Go) or set a shared
doneflag and broadcast (Java/Python). Workers exit when they see it.
Forgetting this is the most common bug in coordination code. Always end the lesson with "and how do these threads shut down?".
Implementations
One coordinator thread (here, integrated into the loop) increments the counter and releases whichever thread should print. Each worker thread blocks on its own semaphore, prints, and releases coordinator semaphore.
1 import java.util.concurrent.Semaphore;
2
3 class FizzBuzz {
4 private final int n;
5 private int current = 1;
6 private final Semaphore numSem = new Semaphore(0);
7 private final Semaphore fizzSem = new Semaphore(0);
8 private final Semaphore buzzSem = new Semaphore(0);
9 private final Semaphore fbSem = new Semaphore(0);
10 private final Semaphore done = new Semaphore(0);
11
12 public FizzBuzz(int n) { this.n = n; }
13
14 public void dispatcher() {
15 for (int i = 1; i <= n; i++) {
16 current = i;
17 if (i % 15 == 0) fbSem.release();
18 else if (i % 3 == 0) fizzSem.release();
19 else if (i % 5 == 0) buzzSem.release();
20 else numSem.release();
21 done.acquireUninterruptibly(); // wait until printed
22 }
23 }
24 public void number(IntConsumer print) throws InterruptedException {
25 while (countMatching(i -> i%3 != 0 && i%5 != 0)) {
26 numSem.acquire(); print.accept(current); done.release();
27 }
28 }
29 // fizz, buzz, fizzbuzz follow the same shape
30 }Key points
- •Four threads, one shared counter, four semaphores
- •A 'dispatcher' increments the counter and releases exactly the matching thread's semaphore
- •Or: each thread waits on a condition that matches its rule (i % 3, i % 5, i % 15)
- •Same token-passing pattern as FooBar but routed by predicate