Backend Development 15 min read

Java Multithreaded Sequential Printing: Solutions Using Lock, wait/notify, Condition, Semaphore, and LockSupport

This article presents various Java solutions for common interview multithreading problems that require sequential printing, demonstrating implementations using Lock, wait/notify, Condition, Semaphore, and LockSupport, along with detailed code examples and explanations of thread communication mechanisms.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Java Multithreaded Sequential Printing: Solutions Using Lock, wait/notify, Condition, Semaphore, and LockSupport

The article explains how to solve typical interview questions that require multiple threads to print characters or numbers in a specific order, focusing on Java concurrency primitives and providing complete code examples for each approach.

Using Lock : A ReentrantLock controls access to a shared state variable; each thread prints its character when state % 3 == targetNum . The implementation ensures correct ordering by incrementing state and releasing the lock.

public class PrintABCUsingLock {
    private int times; // control print count
    private int state; // current state to alternate threads
    private Lock lock = new ReentrantLock();

    public PrintABCUsingLock(int times) {
        this.times = times;
    }

    private void printLetter(String name, int targetNum) {
        for (int i = 0; i < times; ) {
            lock.lock();
            if (state % 3 == targetNum) {
                state++;
                i++;
                System.out.print(name);
            }
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintABCUsingLock loopThread = new PrintABCUsingLock(1);
        new Thread(() -> { loopThread.printLetter("B", 1); }, "B").start();
        new Thread(() -> { loopThread.printLetter("A", 0); }, "A").start();
        new Thread(() -> { loopThread.printLetter("C", 2); }, "C").start();
    }
}

Using wait/notify : Threads synchronize on a common monitor object; each thread waits until state % 3 matches its target, then prints and notifies all waiting threads.

public class PrintABCUsingWaitNotify {
    private int state;
    private int times;
    private static final Object LOCK = new Object();

    public PrintABCUsingWaitNotify(int times) { this.times = times; }

    public static void main(String[] args) {
        PrintABCUsingWaitNotify printABC = new PrintABCUsingWaitNotify(10);
        new Thread(() -> { printABC.printLetter("A", 0); }, "A").start();
        new Thread(() -> { printABC.printLetter("B", 1); }, "B").start();
        new Thread(() -> { printABC.printLetter("C", 2); }, "C").start();
    }

    private void printLetter(String name, int targetState) {
        for (int i = 0; i < times; i++) {
            synchronized (LOCK) {
                while (state % 3 != targetState) {
                    try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
                }
                state++;
                System.out.print(name);
                LOCK.notifyAll();
            }
        }
    }
}

Using Condition : A ReentrantLock with three Condition objects replaces wait/notify . Each thread awaits its condition and signals the next, achieving precise wake‑ups.

public class PrintABCUsingLockCondition {
    private int times;
    private int state;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    public PrintABCUsingLockCondition(int times) { this.times = times; }

    public static void main(String[] args) {
        PrintABCUsingLockCondition print = new PrintABCUsingLockCondition(10);
        new Thread(() -> { print.printLetter("A", 0, c1, c2); }, "A").start();
        new Thread(() -> { print.printLetter("B", 1, c2, c3); }, "B").start();
        new Thread(() -> { print.printLetter("C", 2, c3, c1); }, "C").start();
    }

    private void printLetter(String name, int targetState, Condition current, Condition next) {
        for (int i = 0; i < times; ) {
            lock.lock();
            try {
                while (state % 3 != targetState) { current.await(); }
                state++; i++; System.out.print(name); next.signal();
            } catch (Exception e) { e.printStackTrace(); }
            finally { lock.unlock(); }
        }
    }
}

Using Semaphore (first problem) : Three semaphores form a token ring; only the thread whose semaphore is available proceeds, prints its character, then releases the next semaphore.

public class PrintABCUsingSemaphore {
    private int times;
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(0);
    private static Semaphore semaphoreC = new Semaphore(0);

    public PrintABCUsingSemaphore(int times) { this.times = times; }

    public static void main(String[] args) {
        PrintABCUsingSemaphore printer = new PrintABCUsingSemaphore(1);
        new Thread(() -> { printer.print("A", semaphoreA, semaphoreB); }, "A").start();
        new Thread(() -> { printer.print("B", semaphoreB, semaphoreC); }, "B").start();
        new Thread(() -> { printer.print("C", semaphoreC, semaphoreA); }, "C").start();
    }

    private void print(String name, Semaphore current, Semaphore next) {
        for (int i = 0; i < times; i++) {
            try {
                System.out.println("111" + Thread.currentThread().getName());
                current.acquire();
                System.out.print(name);
                next.release();
                System.out.println("222" + Thread.currentThread().getName());
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

Using Semaphore for N threads : An array of semaphores creates a circular chain; each thread acquires the previous semaphore, prints its index, then releases its own semaphore, enabling ordered printing of numbers from 0 to a limit.

public class LoopPrinter {
    private final static int THREAD_COUNT = 3;
    static int result = 0;
    static int maxNum = 10;

    public static void main(String[] args) throws InterruptedException {
        final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            semaphores[i] = new Semaphore(1);
            if (i != THREAD_COUNT - 1) { semaphores[i].acquire(); }
        }
        for (int i = 0; i < THREAD_COUNT; i++) {
            final Semaphore lastSemphore = i == 0 ? semaphores[THREAD_COUNT - 1] : semaphores[i - 1];
            final Semaphore currentSemphore = semaphores[i];
            final int index = i;
            new Thread(() -> {
                try {
                    while (true) {
                        lastSemphore.acquire();
                        System.out.println("thread" + index + ": " + result++);
                        if (result > maxNum) { System.exit(0); }
                        currentSemphore.release();
                    }
                } catch (Exception e) { e.printStackTrace(); }
            }).start();
        }
    }
}

Using LockSupport : Threads park and unpark each other in a round‑robin fashion, eliminating the need for explicit lock objects.

public class PrintABCUsingLockSupport {
    private static Thread threadA, threadB, threadC;

    public static void main(String[] args) {
        threadA = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.print(Thread.currentThread().getName());
                LockSupport.unpark(threadB);
                LockSupport.park();
            }
        }, "A");
        threadB = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                LockSupport.unpark(threadC);
            }
        }, "B");
        threadC = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                LockSupport.unpark(threadA);
            }
        }, "C");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

Number‑and‑Letter printing : Two additional examples show how to alternate printing numbers and letters using a shared lock and using LockSupport, illustrating the same coordination principles for different output patterns.

public class NumAndLetterPrinter {
    private static Thread numThread, letterThread;
    public static void main(String[] args) {
        letterThread = new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                System.out.print((char)('A' + i));
                LockSupport.unpark(numThread);
                LockSupport.park();
            }
        }, "letterThread");
        numThread = new Thread(() -> {
            for (int i = 1; i <= 26; i++) {
                System.out.print(i);
                LockSupport.park();
                LockSupport.unpark(letterThread);
            }
        }, "numThread");
        numThread.start();
        letterThread.start();
    }
}

By practicing these patterns—Lock, wait/notify, Condition, Semaphore, and LockSupport—readers can strengthen their understanding of Java’s concurrency utilities and be well‑prepared for interview challenges that test thread‑communication skills.

javaConcurrencyMultithreadingSemaphoreLockThread Synchronization
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.