Skip to content

Thread questions

Here are 3 classic, frequently asked Java multithreading coding problems ranging from intermediate to advanced difficulty. Each includes the core concept being tested, a step-by-step breakdown, and production-ready Java solutions using modern java.util.concurrent primitives.


Problem 1: Print Even and Odd Numbers Alternately using Two Threads

Concept Tested

Thread synchronization, inter-thread communication using wait() and notify(), or coordination using concurrency constructs like ReentrantLock and Condition.

Problem Statement

Write a program with two threads. Thread 1 should print odd numbers and Thread 2 should print even numbers sequentially up to a given limit $N$ (e.g., 1, 2, 3, 4, ... N).

Step-by-Step Approach

  1. Maintain a shared counter variable initialized to 1 and a maximum limit $N$.
  2. Create two worker threads sharing a single monitor object (or lock).
  3. Odd Thread Logic: In a loop, lock the monitor. If the counter is even, call wait() to release the lock and pause execution. Once notified and counter becomes odd, print the number, increment the counter, and call notify() to wake up the Even thread.
  4. Even Thread Logic: Follow the inverse condition—wait if the counter is odd; print, increment, and notify if the counter is even.

Solution

public class EvenOddPrinter {
    private final int limit;
    private int counter = 1;

    public EvenOddPrinter(int limit) {
        this.limit = limit;
    }

    public synchronized void printOdd() {
        while (counter <= limit) {
            // Wait while it is the turn for an even number
            while (counter % 2 == 0) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            if (counter <= limit) {
                System.out.println(Thread.currentThread().getName() + ": " + counter);
                counter++;
                notify(); // Notify the even thread
            }
        }
    }

    public synchronized void printEven() {
        while (counter <= limit) {
            // Wait while it is the turn for an odd number
            while (counter % 2 != 0) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            if (counter <= limit) {
                System.out.println(Thread.currentThread().getName() + ": " + counter);
                counter++;
                notify(); // Notify the odd thread
            }
        }
    }

    public static void main(String[] args) {
        EvenOddPrinter printer = new EvenOddPrinter(10);

        Thread oddThread = new Thread(printer::printOdd, "Odd-Thread");
        Thread evenThread = new Thread(printer::printEven, "Even-Thread");

        oddThread.start();
        evenThread.start();
    }
}

Problem 2: Implement a Thread-Safe Bounded Blocking Queue (Producer-Consumer)

Concept Tested

Custom data structure concurrency, handling buffer overflow/underflow, preventing race conditions, and choosing explicit locks (ReentrantLock + Condition) over raw synchronized blocks.

Problem Statement

Implement a fixed-capacity FIFO Blocking Queue from scratch with enqueue(item) and dequeue() methods without using Java’s built-in BlockingQueue classes.

Step-by-Step Approach

  1. Use an internal array or linked list along with pointers for head, tail, and current size.
  2. Use a ReentrantLock to protect queue mutations.
  3. Define two separate Condition objects on the lock: notFull and notEmpty.
  4. Enqueue: Acquire the lock. If size == capacity, await on notFull. Put the item at the tail, increment size, and signal notEmpty.
  5. Dequeue: Acquire the lock. If size == 0, await on notEmpty. Remove the item at the head, decrement size, and signal notFull.

Solution

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

public class BoundedBlockingQueue<T> {
    private final Object[] items;
    private int head = 0;
    private int tail = 0;
    private int count = 0;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedBlockingQueue(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("Capacity must be positive");
        }
        this.items = new Object[capacity];
    }

    public void enqueue(T item) throws InterruptedException {
        lock.lock();
        try {
            // Guard against spurious wakeups with a while loop
            while (count == items.length) {
                notFull.await(); // Wait until queue has space
            }
            items[tail] = item;
            tail = (tail + 1) % items.length; // Circular index bump
            count++;

            notEmpty.signal(); // Signal consumer that an item is available
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public T dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // Wait until queue has an item
            }
            T item = (T) items[head];
            items[head] = null; // Help garbage collection
            head = (head + 1) % items.length; // Circular index bump
            count--;

            notFull.signal(); // Signal producer that space is freed
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Problem 3: Multi-Threaded Task Coordinator (Run N Tasks in Order across 3 Threads)

Concept Tested

Fine-grained state tracking, sequence generation across multiple threads (Thread A $\rightarrow$ Thread B $\rightarrow$ Thread C $\rightarrow$ Thread A), and condition signaling.

Problem Statement

Print the string sequence "ABCABCABC..." $K$ times using three distinct threads: ThreadA (prints 'A'), ThreadB (prints 'B'), and ThreadC (prints 'C').

Step-by-Step Approach

  1. Track the turn state using an integer state where $0 \rightarrow \text{'A'}$, $1 \rightarrow \text{'B'}$, and $2 \rightarrow \text{'C'}$.
  2. Share a monitor lock across all three threads.
  3. Pass target state ($0, 1,$ or $2$) and character to print to a unified task implementation.
  4. While the loop count hasn't reached $K$, check if it's the thread's turn. If not, wait. If yes, print the character, update state to (state + 1) % 3, and call notifyAll() so the next waiting thread can run.

Solution

public class SequencePrinter {
    private final int totalRounds;
    private int state = 0; // 0 for A, 1 for B, 2 for C

    public SequencePrinter(int totalRounds) {
        this.totalRounds = totalRounds;
    }

    public synchronized void printChar(char ch, int targetState) {
        for (int i = 0; i < totalRounds; i++) {
            while (state != targetState) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.print(ch);
            state = (state + 1) % 3; // Advance to next target state
            notifyAll(); // Notify other threads checking state condition
        }
    }

    public static void main(String[] args) {
        int rounds = 5;
        SequencePrinter printer = new SequencePrinter(rounds);

        Thread t1 = new Thread(() -> printer.printChar('A', 0), "Thread-A");
        Thread t2 = new Thread(() -> printer.printChar('B', 1), "Thread-B");
        Thread t3 = new Thread(() -> printer.printChar('C', 2), "Thread-C");

        t1.start();
        t2.start();
        t3.start();
    }
}

Key Takeaways for Multithreading Coding Interviews

  • Always use while loops instead of if for wait() / await(): This protects your implementation from spurious wakeups and state shifts between signal and wakeup.
  • Always unlock in finally blocks: When using explicit Lock objects, lock.unlock() must always reside in a finally block to prevent deadlocks when exceptions occur.
  • Minimize lock scope: Perform expensive operations (like network calls or payload construction) outside locked blocks.