A classic thread coordination problem is making two independent threads print numbers in perfect sequential order: one printing odd numbers (1, 3, 5...) and the other printing even numbers (2, 4, 6...). Without proper signaling, the threads will print out of order. Java's BlockingQueue makes this coordination simple and robust.

In this guide, we will translate this concurrency problem into a simple relay-race analogy and trace the code's execution step by step.

Illustration of odd and even threads passing a baton in a relay race
Real-World Analogy: The Number Relay Race

Imagine a track relay race with two runners: Odd Runner and Even Runner. Instead of running laps, they are writing numbers in a notebook in order from 1 to 20.

They use a single **baton** (the ArrayBlockingQueue) to coordinate:

  • Odd Runner starts the game. They write down their number 1 in the notebook, print it, and then hand the baton to the Even Runner by placing the number `1` inside the queue.
  • Even Runner is waiting by the track. They cannot do anything until they receive the baton. As soon as the number `1` is placed in the queue, they take it out, add 1 to it to get 2, write down and print 2, and wait again.
  • Meanwhile, Odd Runner increases their counter to 3 and places it in the queue, passing the baton back.

By using the queue as a baton, they guarantee they never write out of turn!

Walkthrough of the Main Method Scenario

Let's trace how the program coordinates execution step-by-step from the entry point:

Step 1: Setting up the Queue Baton

First, the main thread sets up the shared thread-safe queue with a limit of 20 elements:

BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(20);

It then constructs the Odd and Even runner classes, sharing this single queue instance between them, and starts both threads.

Step 2: Odd Number Thread Hand-Off

The OddNumber thread runs its loop:

  • It prints "Odd Number 1" to the console.
  • It calls blockingQueue.put(1), which places the number in the queue.
  • It increments its local counter: number += 2 (making it 3).
  • In the next iterations, if the queue is full (because the Even thread is slow), blockingQueue.put(...) will automatically block and put the Odd thread to sleep, preventing it from printing ahead of time.
Step 3: Even Number Thread Reception

The EvenNumber thread runs its loop:

  • It starts by calling blockingQueue.take(). If the queue is empty, this call blocks, putting the Even thread to sleep.
  • As soon as the Odd thread puts 1 in the queue, the Even thread wakes up and retrieves it.
  • It increments the value (1 + 1 = 2) and prints: "Even Number 2".
  • If the printed value reaches 20, the loop breaks and the thread exits.

Java Implementation

Below is the complete Java code demonstrating odd/even printing using ArrayBlockingQueue:

package io.practise.accolite;
 
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
 
public class PrintOddEvenUsingTwoThreads {
    public static void main(String[] args) {
 
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(20);
 
        OddNumber oddNumber = new OddNumber(blockingQueue);
        EvenNumber evenNumber = new EvenNumber(blockingQueue);
 
        new Thread(oddNumber).start();
        new Thread(evenNumber).start();
    }
}
 
class OddNumber implements Runnable {
    private BlockingQueue<Integer> blockingQueue;
    static int number = 1;
 
    public OddNumber(BlockingQueue<Integer> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }
 
    @Override
    public void run() {
        try {
            for (int index = 0; index < 20; ++index) {
                blockingQueue.put(number);
                System.out.println("Odd Number " + number);
                number += 2;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
 
class EvenNumber implements Runnable {
    int number = 2;
    private BlockingQueue<Integer> blockingQueue;
 
    public EvenNumber(BlockingQueue<Integer> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }
 
    @Override
    public void run() {
        try {
            while (true) {
                Integer take = blockingQueue.take();
                take++;
                System.out.println("Even Number " + take);
                if (take == 20) {
                    break;
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Conclusion

Using a BlockingQueue eliminates the need for low-level synchronized, wait, and notify blocks. The queue handles thread state blocking and thread-safe messaging internally, providing a clean, concurrent design that avoids race conditions and index-ordering bugs.