Concurrency

Print Zero-Odd-Even — Three-Thread Sequencing

Three threads must interleave to print '0102030405…' — zero before every number, odd and even alternating. Solved with ReentrantLock and three dedicated Condition variables.

August 10, 2026

Problem#

One object, three methods, three threads — each method called in a loop by its own thread:

ThreadMethodResponsibility
ZeroprintZero()Prints 0 before each number (called n times)
OddprintOdd()Prints odd numbers 1, 3, 5, …
EvenprintEven()Prints even numbers 2, 4, 6, …

For n = 5, the output must be:

0102030405

Read as pairs: 0,1 0,2 0,3 0,4 0,5 — zero fires before every number, and odd/even alternate. The turn sequence is:

zero → odd → zero → even → zero → odd → zero → even → zero → odd

Three threads, but not a simple rotation — zero runs twice as often as odd or even.


Think Before Coding#

Work through these before looking at the solution:

  1. What state do you need? Not just "whose turn" — you need to know:

    • Is it zero's turn, or one of the others?
    • If it's not zero's turn, is the pending number odd or even? Two variables: a zeroTurn boolean and curr % 2 for parity.
  2. Why per-thread signaling? With one shared condition (like notifyAll()), every thread wakes on every change and must re-check — wasteful and error-prone. Three dedicated Conditions (one per thread) let each thread park on its own private channel, and the signaling thread targets exactly the right party.

  3. Why doesn't this work with just two semaphores? Two semaphores handle two-thread alternation cleanly (see FooBar). Here we have three threads with an unequal turn distribution — zero's turn comes twice as often. A pair of semaphores can't cleanly encode that asymmetry without extra state.


Implementation#

java
package ZeroOddEven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ZeroOddEven {
    private int curr = 1;           // next number to print
    private final int max;
    private boolean zeroTurn = true;  // true = zero should print next

    private final ReentrantLock lock   = new ReentrantLock();
    private final Condition zeroCondition = lock.newCondition();
    private final Condition oddCondition  = lock.newCondition();
    private final Condition evenCondition = lock.newCondition();

    public ZeroOddEven(int n) { this.max = n; }

    public void printZero() throws InterruptedException {
        while (true) {
            lock.lock();
            try {
                // Wait while: still in range AND not zero's turn
                while (curr <= max && !zeroTurn)
                    zeroCondition.await();

                if (curr > max) {                  // termination
                    oddCondition.signal();
                    evenCondition.signal();
                    return;
                }

                System.out.print(0);
                zeroTurn = false;

                // Signal the right thread based on next number's parity
                if (curr % 2 == 0) evenCondition.signal();
                else               oddCondition.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public void printOdd() throws InterruptedException {
        while (true) {
            lock.lock();
            try {
                // Wait while: still in range AND (it's zero's turn OR curr is even)
                while (curr <= max && (zeroTurn || curr % 2 == 0))
                    oddCondition.await();

                if (curr > max) {                  // termination
                    zeroCondition.signal();
                    evenCondition.signal();
                    return;
                }

                System.out.print(curr);
                curr++;
                zeroTurn = true;
                zeroCondition.signal();            // zero always goes next
            } finally {
                lock.unlock();
            }
        }
    }

    public void printEven() throws InterruptedException {
        while (true) {
            lock.lock();
            try {
                // Wait while: still in range AND (it's zero's turn OR curr is odd)
                while (curr <= max && (zeroTurn || curr % 2 != 0))
                    evenCondition.await();

                if (curr > max) {                  // termination
                    zeroCondition.signal();
                    oddCondition.signal();
                    return;
                }

                System.out.print(curr);
                curr++;
                zeroTurn = true;
                zeroCondition.signal();            // zero always goes next
            } finally {
                lock.unlock();
            }
        }
    }
}

Key Design Decisions#

Two-part state: zeroTurn + parity

zeroTurn = true → zero prints next. zeroTurn = false → check curr % 2 to decide between odd and even. This two-variable state directly encodes the asymmetric turn sequence.

Three Conditions off one ReentrantLock

One lock, three private waiting channels. Each thread parks only on its own Condition. The signaling thread uses signal() (not signalAll()) to wake exactly the right party — no wasted wakeups, no re-checking against the wrong condition.

signal() not signalAll() in steady state

Because each thread waits on its own dedicated Condition, the signaling thread always knows exactly who to wake. signal() on oddCondition wakes only the odd thread; signal() on evenCondition wakes only the even thread.

Termination: signal the other two

When curr > max, the thread that discovers this was woken by a normal handoff — but the other two threads may still be parked. Before returning, each method signals the other two conditions so those threads also wake, re-check curr > max, and exit their own loops cleanly. Without this, any thread whose turn never comes again would wait forever.

while loop around await()

Required for the same reasons as always: Condition.await() can return spuriously, and even a legitimate signal may have been caused by a condition change that doesn't apply to this thread (e.g., a termination signal reaching the wrong thread's condition branch).


Demo#

java
package ZeroOddEven;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        int n = 5;
        ZeroOddEven printer = new ZeroOddEven(n);

        Thread zeroThread = new Thread(() -> {
            try { printer.printZero(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });
        Thread oddThread = new Thread(() -> {
            try { printer.printOdd(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });
        Thread evenThread = new Thread(() -> {
            try { printer.printEven(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        // Scrambled start order — output ordering is enforced, not accidental
        evenThread.start();
        oddThread.start();
        zeroThread.start();

        zeroThread.join();
        oddThread.join();
        evenThread.join();
        System.out.println();
    }
}

Expected output (n = 5)#

0102030405

Even and odd threads start before zero — they immediately block on their conditions since zeroTurn = true, and zero runs first regardless.


Common Mistakes#

MistakeWhy it breaks
Single condition for all three threadsMust use notifyAll(), which wakes all — each thread re-checks and mostly goes back to sleep; noisy and error-prone
signal() on wrong conditionE.g., signaling evenCondition when next number is odd — the odd thread stays parked, zero never gets a signal back, deadlock
Missing the two-part wait condition in odd/evenIf odd just waits on !zeroTurn, it doesn't check parity — even numbers get printed by the odd thread
No termination signals to the other twoThreads whose turn never comes again sleep forever — program hangs after the last number
Forgetting lock.unlock() in finallyException inside the lock body leaves it held forever — every other thread blocks
if instead of while around await()Spurious wakeup or misdirected signal causes a thread to proceed when conditions aren't met