From 94306c50ab3188fe464c9c9d41bf68d4e1d28ad8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 18:09:50 +0000 Subject: [PATCH] Add multithreading module: odd-even, ping-pong, ordered threads, deadlock detection and fixes, virtual-thread variants (25 tests) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy --- README.md | 1 + multithreading/README.md | 49 ++++++++++ multithreading/output/01-odd-even.txt | 6 ++ multithreading/output/02-ping-pong.txt | 2 + multithreading/output/03-ordered-threads.txt | 5 ++ multithreading/output/04-deadlock.txt | 14 +++ .../output/05-virtual-single-carrier.txt | 6 ++ multithreading/output/06-test-results.txt | 26 ++++++ multithreading/pom.xml | 27 ++++++ multithreading/scripts/run-all.sh | 21 +++++ multithreading/scripts/run.sh | 11 +++ .../interviewlab/multithreading/Account.java | 30 +++++++ .../multithreading/DeadlockDetector.java | 46 ++++++++++ .../multithreading/DeadlockScenario.java | 63 +++++++++++++ .../interviewlab/multithreading/Demo.java | 71 +++++++++++++++ .../interviewlab/multithreading/Group.java | 59 ++++++++++++ .../multithreading/OddEvenLockCondition.java | 47 ++++++++++ .../multithreading/OddEvenPrinter.java | 14 +++ .../multithreading/OddEvenSemaphore.java | 36 ++++++++ .../multithreading/OddEvenWaitNotify.java | 35 ++++++++ .../multithreading/OrderedThreads.java | 69 ++++++++++++++ .../interviewlab/multithreading/PingPong.java | 46 ++++++++++ .../multithreading/ThreadKind.java | 10 +++ .../multithreading/Transfers.java | 89 +++++++++++++++++++ .../multithreading/DeadlockTest.java | 69 ++++++++++++++ .../multithreading/OddEvenTest.java | 61 +++++++++++++ .../multithreading/OrderedThreadsTest.java | 44 +++++++++ .../multithreading/PingPongTest.java | 29 ++++++ .../SingleCarrierVirtualThreadTest.java | 35 ++++++++ 29 files changed, 1021 insertions(+) create mode 100644 multithreading/README.md create mode 100644 multithreading/output/01-odd-even.txt create mode 100644 multithreading/output/02-ping-pong.txt create mode 100644 multithreading/output/03-ordered-threads.txt create mode 100644 multithreading/output/04-deadlock.txt create mode 100644 multithreading/output/05-virtual-single-carrier.txt create mode 100644 multithreading/output/06-test-results.txt create mode 100644 multithreading/pom.xml create mode 100755 multithreading/scripts/run-all.sh create mode 100755 multithreading/scripts/run.sh create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Account.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockDetector.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockScenario.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Demo.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Group.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenLockCondition.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenPrinter.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenSemaphore.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenWaitNotify.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OrderedThreads.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/PingPong.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/ThreadKind.java create mode 100644 multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Transfers.java create mode 100644 multithreading/src/test/java/com/ankurm/interviewlab/multithreading/DeadlockTest.java create mode 100644 multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OddEvenTest.java create mode 100644 multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OrderedThreadsTest.java create mode 100644 multithreading/src/test/java/com/ankurm/interviewlab/multithreading/PingPongTest.java create mode 100644 multithreading/src/test/java/com/ankurm/interviewlab/multithreading/SingleCarrierVirtualThreadTest.java diff --git a/README.md b/README.md index 261b82c..7d7b881 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ One module per article. This repository currently holds: | [`hibernate-jpa`](hibernate-jpa/) | Top 40 Hibernate and JPA Interview Questions (2026) with Runnable Answers | this commit | | [`junit-mockito`](junit-mockito/) | Top 30 JUnit 6 and Mockito Interview Questions (2026) | this commit | | [`hashmap-vs-concurrenthashmap`](hashmap-vs-concurrenthashmap/) | HashMap vs ConcurrentHashMap in Java: Internals, Thread Safety and Interview Answers | this commit | +| [`multithreading`](multithreading/) | Java Multithreading Programs for Interviews: Odd-Even Printing, Ping-Pong, Ordered Threads, Deadlock | this commit | ## `collections` module diff --git a/multithreading/README.md b/multithreading/README.md new file mode 100644 index 0000000..dd959fe --- /dev/null +++ b/multithreading/README.md @@ -0,0 +1,49 @@ +# multithreading + +Classic multithreading interview programs in plain Java 25, each runnable and each pinned by a +JUnit test with a hard timeout (no `sleep()` is used to synchronise anything). Companion to +[Java Multithreading Programs for Interviews](https://ankurm.com/java-multithreading-programs-interview-odd-even-ping-pong-deadlock/). + +## Versions + +| Component | Version | Verified against | +|---|---|---| +| JDK | 25.0.5+8 (Temurin LTS), `--release 25` | `java -version` | +| JUnit Jupiter | 6.1.3 | same version as the other modules in this repo | +| Maven | 3.9.11 | `mvn -version` | + +## Run it + +```bash +cd multithreading +./scripts/run.sh odd-even # also: ping-pong | ordered | deadlock | virtual +mvn test # 25 tests +./scripts/run-all.sh # regenerates everything in output/ +``` + +## Programs + +| Question | Class | Technique | +|---|---|---| +| Odd-even printing | `OddEvenWaitNotify`, `OddEvenSemaphore`, `OddEvenLockCondition` | `synchronized`+`wait/notifyAll`; two `Semaphore`s; `ReentrantLock`+2 `Condition`s | +| Ping-pong | `PingPong` | `ReentrantLock`+2 `Condition`s, boolean turn flag | +| Ordered threads | `OrderedThreads` | `CountDownLatch` chain; `join()` chain; `Semaphore` ring (3 or 1,000 threads) | +| Deadlock | `Transfers`, `DeadlockScenario`, `DeadlockDetector` | naive vs lock ordering vs `tryLock(timeout)`; detection by `ThreadMXBean.findDeadlockedThreads` | +| Virtual threads | every class takes a `ThreadKind` (`PLATFORM` / `VIRTUAL`) | same code, both thread types | + +Support: `Group` starts named threads and waits with a deadline (a hang becomes a failing test). + +## Captured output (`output/`) + +| File | What it shows | +|---|---| +| `01-odd-even.txt` | the three odd-even implementations, 1..10 | +| `02-ping-pong.txt` | 4 rounds of ping/pong | +| `03-ordered-threads.txt` | T3,T2,T1 started in that order, run T1,T2,T3; round-robin of 3 threads | +| `04-deadlock.txt` | real `findDeadlockedThreads` report, both fixes, and virtual threads being invisible to it | +| `05-virtual-single-carrier.txt` | wait/notify and 1,000 virtual threads on ONE carrier thread | +| `06-test-results.txt` | every test name, pass/fail | + +## License + +MIT, see the repository root. diff --git a/multithreading/output/01-odd-even.txt b/multithreading/output/01-odd-even.txt new file mode 100644 index 0000000..ef657d6 --- /dev/null +++ b/multithreading/output/01-odd-even.txt @@ -0,0 +1,6 @@ +OddEvenWaitNotify (platform threads), max=10 + [odd:1, even:2, odd:3, even:4, odd:5, even:6, odd:7, even:8, odd:9, even:10] +OddEvenSemaphore (platform threads), max=10 + [odd:1, even:2, odd:3, even:4, odd:5, even:6, odd:7, even:8, odd:9, even:10] +OddEvenLockCondition (platform threads), max=10 + [odd:1, even:2, odd:3, even:4, odd:5, even:6, odd:7, even:8, odd:9, even:10] diff --git a/multithreading/output/02-ping-pong.txt b/multithreading/output/02-ping-pong.txt new file mode 100644 index 0000000..f0092ac --- /dev/null +++ b/multithreading/output/02-ping-pong.txt @@ -0,0 +1,2 @@ +PingPong, 4 rounds + [ping, pong, ping, pong, ping, pong, ping, pong] diff --git a/multithreading/output/03-ordered-threads.txt b/multithreading/output/03-ordered-threads.txt new file mode 100644 index 0000000..605aaf0 --- /dev/null +++ b/multithreading/output/03-ordered-threads.txt @@ -0,0 +1,5 @@ +Started T3, T2, T1 (in that order). Execution order: + latches: [T1, T2, T3] + join chain: [T1, T2, T3] +Round robin, 3 threads, max=10: + [worker-1:1, worker-2:2, worker-3:3, worker-1:4, worker-2:5, worker-3:6, worker-1:7, worker-2:8, worker-3:9, worker-1:10] diff --git a/multithreading/output/04-deadlock.txt b/multithreading/output/04-deadlock.txt new file mode 100644 index 0000000..a2953cd --- /dev/null +++ b/multithreading/output/04-deadlock.txt @@ -0,0 +1,14 @@ +== NAIVE == + findDeadlockedThreads: DEADLOCK + transfer-A-to-B is WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync owned by transfer-B-to-A + transfer-B-to-A is WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync owned by transfer-A-to-B + after interrupting both threads: total balance = 2000 +== ORDERED == + both transfers completed, no deadlock to detect + balances A=930 B=1070 +== TRY_LOCK == + both transfers completed, no deadlock to detect + balances A=930 B=1070 retries>0=true +== NAIVE on virtual threads == + thread states: [WAITING, WAITING] + findDeadlockedThreads: none (virtual threads are invisible to it) diff --git a/multithreading/output/05-virtual-single-carrier.txt b/multithreading/output/05-virtual-single-carrier.txt new file mode 100644 index 0000000..4b2a596 --- /dev/null +++ b/multithreading/output/05-virtual-single-carrier.txt @@ -0,0 +1,6 @@ +carrier parallelism = 1 +OddEvenWaitNotify on virtual threads, max=10 + [odd:1, even:2, odd:3, even:4, odd:5, even:6, odd:7, even:8, odd:9, even:10] +Round robin: 1000 virtual threads, max=3000 -> 3000 events + first 3: [worker-1:1, worker-2:2, worker-3:3] + last 3: [worker-998:2998, worker-999:2999, worker-1000:3000] diff --git a/multithreading/output/06-test-results.txt b/multithreading/output/06-test-results.txt new file mode 100644 index 0000000..51de37f --- /dev/null +++ b/multithreading/output/06-test-results.txt @@ -0,0 +1,26 @@ +PASS DeadlockTest.naiveLockingDeadlocksAndTheJvmSaysSo +PASS DeadlockTest.consistentLockOrderingRemovesTheCycle +PASS DeadlockTest.fixesAlsoWorkOnVirtualThreads +PASS DeadlockTest.tryLockWithBackOffTurnsDeadlockIntoCountedRetries +PASS DeadlockTest.deadlockedVirtualThreadsAreInvisibleToFindDeadlockedThreads +PASS OddEvenTest.everyImplementationPrintsExactlyOneToTwentyInOrder(ThreadKind)[1] +PASS OddEvenTest.everyImplementationPrintsExactlyOneToTwentyInOrder(ThreadKind)[2] +PASS OddEvenTest.twoDifferentThreadsDoTheWork(ThreadKind)[1] +PASS OddEvenTest.twoDifferentThreadsDoTheWork(ThreadKind)[2] +PASS OddEvenTest.stableAcrossManyRunsAndOddMax(ThreadKind)[1] +PASS OddEvenTest.stableAcrossManyRunsAndOddMax(ThreadKind)[2] +PASS OddEvenTest.zeroAndOneAreHandled(ThreadKind)[1] +PASS OddEvenTest.zeroAndOneAreHandled(ThreadKind)[2] +PASS OrderedThreadsTest.roundRobinOfThreeThreadsPrintsOneToTen(ThreadKind)[1] +PASS OrderedThreadsTest.roundRobinOfThreeThreadsPrintsOneToTen(ThreadKind)[2] +PASS OrderedThreadsTest.latchesRunT1T2T3EvenThoughT3StartsFirst(ThreadKind)[1] +PASS OrderedThreadsTest.latchesRunT1T2T3EvenThoughT3StartsFirst(ThreadKind)[2] +PASS OrderedThreadsTest.thousandVirtualThreadsStillPrintInOrder +PASS OrderedThreadsTest.joinChainRunsT1T2T3(ThreadKind)[1] +PASS OrderedThreadsTest.joinChainRunsT1T2T3(ThreadKind)[2] +PASS PingPongTest.zeroRoundsPrintNothing(ThreadKind)[1] +PASS PingPongTest.zeroRoundsPrintNothing(ThreadKind)[2] +PASS PingPongTest.alternatesStrictlyForGivenRounds(ThreadKind)[1] +PASS PingPongTest.alternatesStrictlyForGivenRounds(ThreadKind)[2] +PASS SingleCarrierVirtualThreadTest.waitNotifyFinishesOnOneCarrierThread +25 tests, 0 failures diff --git a/multithreading/pom.xml b/multithreading/pom.xml new file mode 100644 index 0000000..079c5f2 --- /dev/null +++ b/multithreading/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.ankurm.interviewlab + multithreading + 1.0.0 + multithreading + Classic multithreading interview programs: odd-even, ping-pong, ordered threads, deadlock + + + 25 + UTF-8 + 6.1.3 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/multithreading/scripts/run-all.sh b/multithreading/scripts/run-all.sh new file mode 100755 index 0000000..ea6641f --- /dev/null +++ b/multithreading/scripts/run-all.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Regenerates every transcript in output/. Needs JDK 25 and Maven 3.9+. +set -euo pipefail +cd "$(dirname "$0")/.." +mkdir -p output +./scripts/run.sh odd-even > output/01-odd-even.txt +./scripts/run.sh ping-pong > output/02-ping-pong.txt +./scripts/run.sh ordered > output/03-ordered-threads.txt +./scripts/run.sh deadlock > output/04-deadlock.txt +./scripts/run.sh virtual > output/05-virtual-single-carrier.txt +mvn -B -q test > /dev/null 2>&1 +python3 - <<'PY' > output/06-test-results.txt +import glob, xml.etree.ElementTree as ET +total = fail = 0 +for f in sorted(glob.glob("target/surefire-reports/TEST-*.xml")): + for tc in ET.parse(f).getroot().iter("testcase"): + bad = tc.find("failure") is not None or tc.find("error") is not None + total += 1; fail += bad + print(("FAIL " if bad else "PASS ") + tc.get("classname").split(".")[-1] + "." + tc.get("name")) +print(f"{total} tests, {fail} failures") +PY diff --git a/multithreading/scripts/run.sh b/multithreading/scripts/run.sh new file mode 100755 index 0000000..fd2685f --- /dev/null +++ b/multithreading/scripts/run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Runs one demo: ./scripts/run.sh odd-even | ping-pong | ordered | deadlock | virtual +# Needs JDK 25 and Maven (Maven only to compile). The "virtual" demo is started with ONE carrier thread. +set -euo pipefail +cd "$(dirname "$0")/.." +mvn -B -q compile 2>&1 | grep -E 'ERROR' || true +FLAGS=(-Xmx256m) +if [ "${1:-}" = "virtual" ]; then + FLAGS+=(-Djdk.virtualThreadScheduler.parallelism=1 -Djdk.virtualThreadScheduler.maxPoolSize=1) +fi +java "${FLAGS[@]}" -cp target/classes com.ankurm.interviewlab.multithreading.Demo "$1" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Account.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Account.java new file mode 100644 index 0000000..a431e62 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Account.java @@ -0,0 +1,30 @@ +package com.ankurm.interviewlab.multithreading; + +import java.util.concurrent.locks.ReentrantLock; + +/** A bank account with its own lock. The id gives every account a global order (used by the fix). */ +public final class Account { + final int id; + final String name; + final ReentrantLock lock = new ReentrantLock(); + private long balance; // guarded by lock + + public Account(int id, String name, long balance) { + this.id = id; + this.name = name; + this.balance = balance; + } + + public String name() { return name; } + + /** Callers must hold the lock. */ + void withdraw(long amount) { balance -= amount; } + + /** Callers must hold the lock. */ + void deposit(long amount) { balance += amount; } + + public long balance() { + lock.lock(); + try { return balance; } finally { lock.unlock(); } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockDetector.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockDetector.java new file mode 100644 index 0000000..2d9aa79 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockDetector.java @@ -0,0 +1,46 @@ +package com.ankurm.interviewlab.multithreading; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** Finds deadlocked threads with ThreadMXBean.findDeadlockedThreads (covers synchronized AND j.u.c locks). */ +public final class DeadlockDetector { + + private static final ThreadMXBean MX = ManagementFactory.getThreadMXBean(); + + private DeadlockDetector() { } + + /** Polls until the JVM reports a deadlock, or the timeout passes. Detection is the thing under test, so polling is fine. */ + public static Optional> awaitDeadlock(Duration timeout) throws InterruptedException { + long deadline = System.nanoTime() + timeout.toNanos(); + while (System.nanoTime() < deadline) { + long[] ids = MX.findDeadlockedThreads(); // null when there is no deadlock + if (ids != null) { + return Optional.of(describe(ids)); + } + Thread.sleep(5); + } + return Optional.empty(); + } + + /** One line per deadlocked thread: name, state, the lock type it waits for, and who owns it. */ + static List describe(long[] ids) { + List lines = new ArrayList<>(); + for (ThreadInfo info : MX.getThreadInfo(ids, true, true)) { + String lockType = info.getLockInfo() == null ? "?" : info.getLockInfo().getClassName(); + lines.add(info.getThreadName() + " is " + info.getThreadState() + + " on " + lockType + " owned by " + info.getLockOwnerName()); + } + lines.sort(null); // thread-id order is not stable; name order is + return lines; + } + + public static boolean anyDeadlock() { + return MX.findDeadlockedThreads() != null; + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockScenario.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockScenario.java new file mode 100644 index 0000000..6e6e6fa --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/DeadlockScenario.java @@ -0,0 +1,63 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicInteger; + +/** Wires two opposite transfers (A->B and B->A) and runs them with a chosen locking strategy. */ +public final class DeadlockScenario { + + public enum Strategy { NAIVE, ORDERED, TRY_LOCK } + + /** deadlock is empty when none was reported; stuckStates are the two threads' states at check time. */ + public record Result(Optional> deadlock, List stuckStates, + long balanceA, long balanceB, int retries) { } + + private DeadlockScenario() { } + + /** + * @param detectFor how long NAIVE waits for findDeadlockedThreads to report (ignored by the fixes) + */ + public static Result run(Strategy strategy, ThreadKind kind, Duration detectFor) throws Exception { + Account a = new Account(1, "A", 1000); + Account b = new Account(2, "B", 1000); + CyclicBarrier bothHoldFirstLock = new CyclicBarrier(2); + // With ordered locking the second thread cannot even take its "first" lock until the first + // thread is finished, so a barrier there would wait forever: the fix has no interleaving to force. + Runnable hook = strategy == Strategy.ORDERED ? () -> { } : () -> { + try { bothHoldFirstLock.await(); } catch (Exception e) { throw new IllegalStateException(e); } + }; + AtomicInteger retries = new AtomicInteger(); + + Group group = new Group(kind); + group.start("transfer-A-to-B", () -> move(strategy, a, b, 100, hook, retries)); + group.start("transfer-B-to-A", () -> move(strategy, b, a, 30, hook, retries)); + + Optional> deadlock = Optional.empty(); + List states = List.of(); + if (strategy == Strategy.NAIVE) { + // Wait until both threads are parked on the second lock, then ask the JVM. + long limit = System.nanoTime() + Duration.ofSeconds(5).toNanos(); + while (group.threads().stream().anyMatch(t -> t.getState() != Thread.State.WAITING) + && System.nanoTime() < limit) { + Thread.sleep(2); + } + states = group.threads().stream().map(Thread::getState).toList(); + deadlock = DeadlockDetector.awaitDeadlock(detectFor); + group.interruptAll(); // lockInterruptibly lets us break the deadlock and clean up + } + group.awaitAll(Duration.ofSeconds(10)); + return new Result(deadlock, states, a.balance(), b.balance(), retries.get()); + } + + private static void move(Strategy s, Account from, Account to, long amt, Runnable hook, + AtomicInteger retries) throws InterruptedException { + switch (s) { + case NAIVE -> Transfers.naive(from, to, amt, hook); + case ORDERED -> Transfers.ordered(from, to, amt, hook); + case TRY_LOCK -> Transfers.withTryLock(from, to, amt, hook, retries); + } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Demo.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Demo.java new file mode 100644 index 0000000..6a004cc --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Demo.java @@ -0,0 +1,71 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.List; + +/** Prints the transcripts under output/. Usage: Demo odd-even | ping-pong | ordered | deadlock | virtual */ +public final class Demo { + + public static void main(String[] args) throws Exception { + switch (args.length == 0 ? "" : args[0]) { + case "odd-even" -> oddEven(); + case "ping-pong" -> pingPong(); + case "ordered" -> ordered(); + case "deadlock" -> deadlock(); + case "virtual" -> virtual(); + default -> throw new IllegalArgumentException("odd-even | ping-pong | ordered | deadlock | virtual"); + } + } + + private static void oddEven() throws Exception { + for (OddEvenPrinter p : List.of(new OddEvenWaitNotify(), new OddEvenSemaphore(), new OddEvenLockCondition())) { + System.out.println(p.getClass().getSimpleName() + " (platform threads), max=10"); + System.out.println(" " + p.print(10, ThreadKind.PLATFORM)); + } + } + + private static void pingPong() throws Exception { + System.out.println("PingPong, 4 rounds"); + System.out.println(" " + new PingPong().play(4, ThreadKind.PLATFORM)); + } + + private static void ordered() throws Exception { + System.out.println("Started T3, T2, T1 (in that order). Execution order:"); + System.out.println(" latches: " + OrderedThreads.withLatches(ThreadKind.PLATFORM)); + System.out.println(" join chain: " + OrderedThreads.withJoin(ThreadKind.PLATFORM)); + System.out.println("Round robin, 3 threads, max=10:"); + System.out.println(" " + OrderedThreads.roundRobin(3, 10, ThreadKind.PLATFORM)); + } + + private static void deadlock() throws Exception { + for (DeadlockScenario.Strategy s : DeadlockScenario.Strategy.values()) { + DeadlockScenario.Result r = DeadlockScenario.run(s, ThreadKind.PLATFORM, Duration.ofSeconds(5)); + System.out.println("== " + s + " =="); + if (s == DeadlockScenario.Strategy.NAIVE) { + System.out.println(" findDeadlockedThreads: " + (r.deadlock().isPresent() ? "DEADLOCK" : "none")); + } else { + System.out.println(" both transfers completed, no deadlock to detect"); + } + r.deadlock().ifPresent(lines -> lines.forEach(l -> System.out.println(" " + l))); + System.out.println(s == DeadlockScenario.Strategy.NAIVE + ? " after interrupting both threads: total balance = " + (r.balanceA() + r.balanceB()) + : " balances A=" + r.balanceA() + " B=" + r.balanceB() + + (s == DeadlockScenario.Strategy.TRY_LOCK ? " retries>0=" + (r.retries() > 0) : "")); + } + DeadlockScenario.Result v = DeadlockScenario.run(DeadlockScenario.Strategy.NAIVE, + ThreadKind.VIRTUAL, Duration.ofMillis(500)); + System.out.println("== NAIVE on virtual threads =="); + System.out.println(" thread states: " + v.stuckStates()); + System.out.println(" findDeadlockedThreads: " + (v.deadlock().isPresent() ? "DEADLOCK" : "none (virtual threads are invisible to it)")); + } + + private static void virtual() throws Exception { + System.out.println("carrier parallelism = " + System.getProperty("jdk.virtualThreadScheduler.parallelism", "default")); + System.out.println("OddEvenWaitNotify on virtual threads, max=10"); + System.out.println(" " + new OddEvenWaitNotify().print(10, ThreadKind.VIRTUAL)); + List ring = OrderedThreads.roundRobin(1000, 3000, ThreadKind.VIRTUAL); + System.out.println("Round robin: 1000 virtual threads, max=3000 -> " + ring.size() + " events"); + System.out.println(" first 3: " + ring.subList(0, 3)); + System.out.println(" last 3: " + ring.subList(ring.size() - 3, ring.size())); + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Group.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Group.java new file mode 100644 index 0000000..b471fd5 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Group.java @@ -0,0 +1,59 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Starts named threads, then waits for all of them with a hard deadline. If a worker throws, the + * failure is rethrown from {@link #awaitAll}. If the deadline passes (a lost wake-up, a deadlock) + * the stragglers are interrupted and a TimeoutException is thrown, so a bug fails fast instead of + * hanging the build. + */ +public final class Group { + + @FunctionalInterface + public interface Task { void run() throws Exception; } + + private final ThreadKind kind; + private final List threads = new ArrayList<>(); + private final List failures = new CopyOnWriteArrayList<>(); + + public Group(ThreadKind kind) { this.kind = kind; } + + public Thread start(String name, Task task) { + Thread t = kind.builder(name).unstarted(() -> { + try { + task.run(); + } catch (InterruptedException e) { + // interrupted by awaitAll's cleanup; not a failure of its own + } catch (Throwable e) { + failures.add(e); + } + }); + threads.add(t); + t.start(); + return t; + } + + public List threads() { return List.copyOf(threads); } + + public void interruptAll() { threads.forEach(Thread::interrupt); } + + public void awaitAll(Duration timeout) throws InterruptedException, TimeoutException { + long deadline = System.nanoTime() + timeout.toNanos(); + for (Thread t : threads) { + long left = deadline - System.nanoTime(); + if (left <= 0 || !t.join(Duration.ofNanos(left))) { + threads.forEach(Thread::interrupt); + throw new TimeoutException("thread still running after " + timeout + ": " + t.getName()); + } + } + if (!failures.isEmpty()) { + throw new IllegalStateException("worker failed", failures.get(0)); + } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenLockCondition.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenLockCondition.java new file mode 100644 index 0000000..6d7ea5b --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenLockCondition.java @@ -0,0 +1,47 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Odd-even printing with ReentrantLock and two Conditions (one wait-set per thread), so each side + * signals exactly the other side instead of waking everybody the way notifyAll does. + */ +public final class OddEvenLockCondition implements OddEvenPrinter { + + private final ReentrantLock lock = new ReentrantLock(); + private final Condition oddTurn = lock.newCondition(); + private final Condition evenTurn = lock.newCondition(); + private int next = 1; // guarded by lock + private final List events = new ArrayList<>(); // guarded by lock + + @Override + public List print(int max, ThreadKind kind) throws Exception { + Group group = new Group(kind); + group.start("odd", () -> worker("odd", 1, max, oddTurn, evenTurn)); + group.start("even", () -> worker("even", 2, max, evenTurn, oddTurn)); + group.awaitAll(Duration.ofSeconds(10)); + lock.lock(); + try { return List.copyOf(events); } finally { lock.unlock(); } + } + + private void worker(String name, int first, int max, Condition mine, Condition other) + throws InterruptedException { + for (int n = first; n <= max; n += 2) { + lock.lock(); + try { + while (next != n) { + mine.await(); + } + events.add(name + ":" + n); + next++; + other.signal(); + } finally { + lock.unlock(); + } + } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenPrinter.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenPrinter.java new file mode 100644 index 0000000..2dfbbe4 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenPrinter.java @@ -0,0 +1,14 @@ +package com.ankurm.interviewlab.multithreading; + +import java.util.List; + +/** + * Interview question 1: two threads, one prints odd numbers and one prints even numbers, + * and together they must print 1, 2, 3 ... max in order. + * + * Each implementation returns the events as "threadName:number" in the exact order they happened, + * so a test can assert BOTH the order and which thread printed each number. + */ +public interface OddEvenPrinter { + List print(int max, ThreadKind kind) throws Exception; +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenSemaphore.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenSemaphore.java new file mode 100644 index 0000000..f3265a1 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenSemaphore.java @@ -0,0 +1,36 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.Collections; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Semaphore; + +/** Odd-even printing with two Semaphores passing a single "turn" permit back and forth. */ +public final class OddEvenSemaphore implements OddEvenPrinter { + + @Override + public List print(int max, ThreadKind kind) throws Exception { + Semaphore oddTurn = new Semaphore(1); // odd goes first + Semaphore evenTurn = new Semaphore(0); + List events = Collections.synchronizedList(new ArrayList<>()); + + Group group = new Group(kind); + group.start("odd", () -> { + for (int n = 1; n <= max; n += 2) { + oddTurn.acquire(); + events.add("odd:" + n); + evenTurn.release(); + } + }); + group.start("even", () -> { + for (int n = 2; n <= max; n += 2) { + evenTurn.acquire(); + events.add("even:" + n); + oddTurn.release(); + } + }); + group.awaitAll(Duration.ofSeconds(10)); + return List.copyOf(events); + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenWaitNotify.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenWaitNotify.java new file mode 100644 index 0000000..0212c02 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OddEvenWaitNotify.java @@ -0,0 +1,35 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** Odd-even printing with synchronized + wait()/notifyAll(): the answer the interviewer expects first. */ +public final class OddEvenWaitNotify implements OddEvenPrinter { + + private final Object monitor = new Object(); + private int next = 1; // guarded by monitor + private final List events = new ArrayList<>(); // guarded by monitor + + @Override + public List print(int max, ThreadKind kind) throws Exception { + Group group = new Group(kind); + group.start("even", () -> worker("even", 2, max)); + group.start("odd", () -> worker("odd", 1, max)); // started second on purpose: order must not matter + group.awaitAll(Duration.ofSeconds(10)); + synchronized (monitor) { return List.copyOf(events); } + } + + private void worker(String name, int first, int max) throws InterruptedException { + for (int n = first; n <= max; n += 2) { + synchronized (monitor) { + while (next != n) { // a LOOP, never an if: guards against spurious wake-ups + monitor.wait(); + } + events.add(name + ":" + n); + next++; + monitor.notifyAll(); // notifyAll, not notify: see the post for why + } + } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OrderedThreads.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OrderedThreads.java new file mode 100644 index 0000000..7f4b0e5 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/OrderedThreads.java @@ -0,0 +1,69 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; + +/** + * Interview question 3: three threads are started in the WRONG order (3, 2, 1) but must execute + * T1, then T2, then T3. Plus the round-robin version: N threads print 1..max, thread k printing + * the numbers where (n-1) % N == k-1. + */ +public final class OrderedThreads { + + private OrderedThreads() { } + + /** Version A: each thread waits on a CountDownLatch that its predecessor counts down. */ + public static List withLatches(ThreadKind kind) throws Exception { + List events = Collections.synchronizedList(new ArrayList<>()); + CountDownLatch t1Done = new CountDownLatch(1); + CountDownLatch t2Done = new CountDownLatch(1); + + Group group = new Group(kind); + group.start("T3", () -> { t2Done.await(); events.add("T3"); }); + group.start("T2", () -> { t1Done.await(); events.add("T2"); t2Done.countDown(); }); + group.start("T1", () -> { events.add("T1"); t1Done.countDown(); }); + group.awaitAll(Duration.ofSeconds(10)); + return List.copyOf(events); + } + + /** Version B: the classic Thread.join() chain: T2 joins T1, T3 joins T2. */ + public static List withJoin(ThreadKind kind) throws Exception { + List events = Collections.synchronizedList(new ArrayList<>()); + Group group = new Group(kind); + // Threads must exist before they can be joined, so create T1 first and hand it to T2. + Thread t1 = group.start("T1", () -> events.add("T1")); + Thread t2 = group.start("T2", () -> { t1.join(); events.add("T2"); }); + group.start("T3", () -> { t2.join(); events.add("T3"); }); + group.awaitAll(Duration.ofSeconds(10)); + return List.copyOf(events); + } + + /** + * Version C: N threads in a ring of Semaphores. Thread k waits on permit k, prints, then + * releases permit k+1 (wrapping around). Works the same for 3 threads or 1,000 virtual threads. + */ + public static List roundRobin(int threadCount, int max, ThreadKind kind) throws Exception { + List events = Collections.synchronizedList(new ArrayList<>()); + Semaphore[] turn = new Semaphore[threadCount]; + for (int i = 0; i < threadCount; i++) { + turn[i] = new Semaphore(i == 0 ? 1 : 0); + } + Group group = new Group(kind); + for (int k = 0; k < threadCount; k++) { + final int me = k; + group.start("worker-" + (me + 1), () -> { + for (int n = me + 1; n <= max; n += threadCount) { + turn[me].acquire(); + events.add("worker-" + (me + 1) + ":" + n); + turn[(me + 1) % threadCount].release(); + } + }); + } + group.awaitAll(Duration.ofSeconds(30)); + return List.copyOf(events); + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/PingPong.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/PingPong.java new file mode 100644 index 0000000..1203bc0 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/PingPong.java @@ -0,0 +1,46 @@ +package com.ankurm.interviewlab.multithreading; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Interview question 2: one thread prints "ping", another prints "pong", strictly alternating, + * for a given number of rounds. Same shape as odd-even, but the state is a boolean, not a counter. + */ +public final class PingPong { + + private final ReentrantLock lock = new ReentrantLock(); + private final Condition pingTurn = lock.newCondition(); + private final Condition pongTurn = lock.newCondition(); + private boolean pingsTurn = true; // guarded by lock + private final List events = new ArrayList<>(); // guarded by lock + + public List play(int rounds, ThreadKind kind) throws Exception { + Group group = new Group(kind); + group.start("pong", () -> side("pong", false, rounds, pongTurn, pingTurn)); + group.start("ping", () -> side("ping", true, rounds, pingTurn, pongTurn)); + group.awaitAll(Duration.ofSeconds(10)); + lock.lock(); + try { return List.copyOf(events); } finally { lock.unlock(); } + } + + private void side(String word, boolean isPing, int rounds, Condition mine, Condition other) + throws InterruptedException { + for (int i = 0; i < rounds; i++) { + lock.lock(); + try { + while (pingsTurn != isPing) { + mine.await(); + } + events.add(word); + pingsTurn = !isPing; + other.signal(); + } finally { + lock.unlock(); + } + } + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/ThreadKind.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/ThreadKind.java new file mode 100644 index 0000000..42c619e --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/ThreadKind.java @@ -0,0 +1,10 @@ +package com.ankurm.interviewlab.multithreading; + +/** Every program takes a ThreadKind so the SAME code runs on platform and on virtual threads. */ +public enum ThreadKind { + PLATFORM, VIRTUAL; + + Thread.Builder builder(String name) { + return (this == VIRTUAL ? Thread.ofVirtual() : Thread.ofPlatform()).name(name); + } +} diff --git a/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Transfers.java b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Transfers.java new file mode 100644 index 0000000..3288518 --- /dev/null +++ b/multithreading/src/main/java/com/ankurm/interviewlab/multithreading/Transfers.java @@ -0,0 +1,89 @@ +package com.ankurm.interviewlab.multithreading; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Interview question 4: deadlock. Three ways to move money between two accounts: + * + * 1. {@link #naive} locks "from" then "to". A->B in one thread and B->A in another deadlocks. + * 2. {@link #ordered} always locks the account with the smaller id first. No cycle is possible. + * 3. {@link #withTryLock} locks "from", then tryLock(timeout) on "to"; on failure it lets go of + * everything and retries. Deadlock becomes a (counted) retry. + * + * The {@code afterFirstLock} hook exists so a test can force the unlucky interleaving + * (both threads hold their first lock) on every run instead of one run in a thousand. + */ +public final class Transfers { + + private Transfers() { } + + public static void naive(Account from, Account to, long amount, Runnable afterFirstLock) + throws InterruptedException { + from.lock.lockInterruptibly(); // interruptible only so the demo can clean up afterwards + try { + afterFirstLock.run(); + to.lock.lockInterruptibly(); + try { + from.withdraw(amount); + to.deposit(amount); + } finally { + to.lock.unlock(); + } + } finally { + from.lock.unlock(); + } + } + + public static void ordered(Account from, Account to, long amount, Runnable afterFirstLock) + throws InterruptedException { + Account first = from.id < to.id ? from : to; + Account second = first == from ? to : from; + first.lock.lockInterruptibly(); + try { + afterFirstLock.run(); + second.lock.lockInterruptibly(); + try { + from.withdraw(amount); + to.deposit(amount); + } finally { + second.lock.unlock(); + } + } finally { + first.lock.unlock(); + } + } + + /** Returns after the transfer succeeded; every failed attempt increments {@code retries}. */ + public static void withTryLock(Account from, Account to, long amount, Runnable afterFirstLock, + AtomicInteger retries) throws InterruptedException { + boolean firstAttempt = true; + while (true) { + from.lock.lockInterruptibly(); + boolean done = false; + try { + if (firstAttempt) { + afterFirstLock.run(); + firstAttempt = false; + } + if (to.lock.tryLock(50, TimeUnit.MILLISECONDS)) { + try { + from.withdraw(amount); + to.deposit(amount); + done = true; + } finally { + to.lock.unlock(); + } + } + } finally { + from.lock.unlock(); + } + if (done) { + return; + } + retries.incrementAndGet(); + // Asymmetric back-off so two retrying threads cannot stay in lock-step forever. + Thread.sleep(from.id < to.id ? 0 : 25); + } + } +} diff --git a/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/DeadlockTest.java b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/DeadlockTest.java new file mode 100644 index 0000000..57c748a --- /dev/null +++ b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/DeadlockTest.java @@ -0,0 +1,69 @@ +package com.ankurm.interviewlab.multithreading; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@Timeout(60) +class DeadlockTest { + + @Test + void naiveLockingDeadlocksAndTheJvmSaysSo() throws Exception { + DeadlockScenario.Result r = DeadlockScenario.run(DeadlockScenario.Strategy.NAIVE, + ThreadKind.PLATFORM, Duration.ofSeconds(5)); + List lines = r.deadlock().orElseThrow(() -> new AssertionError("no deadlock reported")); + assertEquals(2, lines.size()); + assertTrue(lines.get(0).startsWith("transfer-A-to-B is WAITING on java.util.concurrent.locks.ReentrantLock")); + assertTrue(lines.get(0).endsWith("owned by transfer-B-to-A")); + assertTrue(lines.get(1).endsWith("owned by transfer-A-to-B")); + // We broke the deadlock by interrupting both threads. That is not atomic: the first interrupt + // can free a lock that the other thread grabs before ITS interrupt lands, so one transfer may + // complete. What must hold either way is that no money was created or lost. + assertEquals(2000, r.balanceA() + r.balanceB()); + assertFalse(DeadlockDetector.anyDeadlock(), "threads were cleaned up"); + } + + @Test + void consistentLockOrderingRemovesTheCycle() throws Exception { + for (int i = 0; i < 100; i++) { + DeadlockScenario.Result r = DeadlockScenario.run(DeadlockScenario.Strategy.ORDERED, + ThreadKind.PLATFORM, Duration.ZERO); + assertEquals(930, r.balanceA()); // 1000 - 100 + 30 + assertEquals(1070, r.balanceB()); + } + } + + @Test + void tryLockWithBackOffTurnsDeadlockIntoCountedRetries() throws Exception { + DeadlockScenario.Result r = DeadlockScenario.run(DeadlockScenario.Strategy.TRY_LOCK, + ThreadKind.PLATFORM, Duration.ZERO); + assertEquals(930, r.balanceA()); + assertEquals(1070, r.balanceB()); + // the barrier guarantees both threads held their first lock, so at least one tryLock had to fail + assertTrue(r.retries() >= 1, "retries=" + r.retries()); + } + + @Test + void fixesAlsoWorkOnVirtualThreads() throws Exception { + for (DeadlockScenario.Strategy s : List.of(DeadlockScenario.Strategy.ORDERED, DeadlockScenario.Strategy.TRY_LOCK)) { + DeadlockScenario.Result r = DeadlockScenario.run(s, ThreadKind.VIRTUAL, Duration.ZERO); + assertEquals(930, r.balanceA()); + assertEquals(1070, r.balanceB()); + } + } + + @Test + void deadlockedVirtualThreadsAreInvisibleToFindDeadlockedThreads() throws Exception { + DeadlockScenario.Result r = DeadlockScenario.run(DeadlockScenario.Strategy.NAIVE, + ThreadKind.VIRTUAL, Duration.ofMillis(500)); + // both virtual threads really are stuck ... + assertEquals(List.of(Thread.State.WAITING, Thread.State.WAITING), r.stuckStates()); + // ... and the MXBean still reports nothing + assertTrue(r.deadlock().isEmpty()); + } +} diff --git a/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OddEvenTest.java b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OddEvenTest.java new file mode 100644 index 0000000..95baa4e --- /dev/null +++ b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OddEvenTest.java @@ -0,0 +1,61 @@ +package com.ankurm.interviewlab.multithreading; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@Timeout(60) +class OddEvenTest { + + private static final List> IMPLEMENTATIONS = List.of( + OddEvenWaitNotify::new, OddEvenSemaphore::new, OddEvenLockCondition::new); + + static List expected(int max) { + List out = new ArrayList<>(); + IntStream.rangeClosed(1, max).forEach(n -> out.add((n % 2 == 1 ? "odd:" : "even:") + n)); + return out; + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void everyImplementationPrintsExactlyOneToTwentyInOrder(ThreadKind kind) throws Exception { + for (Supplier impl : IMPLEMENTATIONS) { + assertEquals(expected(20), impl.get().print(20, kind), impl.get().getClass().getSimpleName()); + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void stableAcrossManyRunsAndOddMax(ThreadKind kind) throws Exception { + // 200 runs per implementation: a lost wake-up or missing loop would hang (-> @Timeout) or reorder. + for (Supplier impl : IMPLEMENTATIONS) { + for (int i = 0; i < 200; i++) { + assertEquals(expected(7), impl.get().print(7, kind)); + } + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void zeroAndOneAreHandled(ThreadKind kind) throws Exception { + for (Supplier impl : IMPLEMENTATIONS) { + assertEquals(List.of(), impl.get().print(0, kind)); + assertEquals(List.of("odd:1"), impl.get().print(1, kind)); + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void twoDifferentThreadsDoTheWork(ThreadKind kind) throws Exception { + List events = new OddEvenSemaphore().print(10, kind); + assertTrue(events.stream().filter(e -> e.startsWith("odd:")).count() == 5); + assertTrue(events.stream().filter(e -> e.startsWith("even:")).count() == 5); + } +} diff --git a/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OrderedThreadsTest.java b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OrderedThreadsTest.java new file mode 100644 index 0000000..972217b --- /dev/null +++ b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/OrderedThreadsTest.java @@ -0,0 +1,44 @@ +package com.ankurm.interviewlab.multithreading; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@Timeout(60) +class OrderedThreadsTest { + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void latchesRunT1T2T3EvenThoughT3StartsFirst(ThreadKind kind) throws Exception { + for (int i = 0; i < 200; i++) { + assertEquals(List.of("T1", "T2", "T3"), OrderedThreads.withLatches(kind)); + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void joinChainRunsT1T2T3(ThreadKind kind) throws Exception { + for (int i = 0; i < 200; i++) { + assertEquals(List.of("T1", "T2", "T3"), OrderedThreads.withJoin(kind)); + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void roundRobinOfThreeThreadsPrintsOneToTen(ThreadKind kind) throws Exception { + List expected = IntStream.rangeClosed(1, 10).mapToObj(n -> "worker-" + ((n - 1) % 3 + 1) + ":" + n).toList(); + assertEquals(expected, OrderedThreads.roundRobin(3, 10, kind)); + } + + @Test + void thousandVirtualThreadsStillPrintInOrder() throws Exception { + List events = OrderedThreads.roundRobin(1000, 5000, ThreadKind.VIRTUAL); + List expected = IntStream.rangeClosed(1, 5000).mapToObj(n -> "worker-" + ((n - 1) % 1000 + 1) + ":" + n).toList(); + assertEquals(expected, events); + } +} diff --git a/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/PingPongTest.java b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/PingPongTest.java new file mode 100644 index 0000000..0a4a1ce --- /dev/null +++ b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/PingPongTest.java @@ -0,0 +1,29 @@ +package com.ankurm.interviewlab.multithreading; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@Timeout(60) +class PingPongTest { + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void alternatesStrictlyForGivenRounds(ThreadKind kind) throws Exception { + List expected = Collections.nCopies(500, List.of("ping", "pong")).stream() + .flatMap(List::stream).toList(); + for (int i = 0; i < 20; i++) { + assertEquals(expected, new PingPong().play(500, kind)); + } + } + + @ParameterizedTest + @EnumSource(ThreadKind.class) + void zeroRoundsPrintNothing(ThreadKind kind) throws Exception { + assertEquals(List.of(), new PingPong().play(0, kind)); + } +} diff --git a/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/SingleCarrierVirtualThreadTest.java b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/SingleCarrierVirtualThreadTest.java new file mode 100644 index 0000000..a41a99d --- /dev/null +++ b/multithreading/src/test/java/com/ankurm/interviewlab/multithreading/SingleCarrierVirtualThreadTest.java @@ -0,0 +1,35 @@ +package com.ankurm.interviewlab.multithreading; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * The scheduler for virtual threads is configured when the JVM starts, so this test launches a + * child JVM with exactly ONE carrier thread. Java 25 (JEP 491) lets a virtual thread blocked in + * Object.wait() give its carrier back, so wait/notify odd-even must still finish. + */ +@Timeout(60) +class SingleCarrierVirtualThreadTest { + + @Test + void waitNotifyFinishesOnOneCarrierThread() throws Exception { + Path java = Path.of(System.getProperty("java.home"), "bin", "java"); + Process p = new ProcessBuilder(java.toString(), "-Xmx256m", + "-Djdk.virtualThreadScheduler.parallelism=1", + "-Djdk.virtualThreadScheduler.maxPoolSize=1", + "-cp", System.getProperty("java.class.path"), + "com.ankurm.interviewlab.multithreading.Demo", "virtual") + .redirectErrorStream(true).start(); + String out = new String(p.getInputStream().readAllBytes()); + assertTrue(p.waitFor(30, TimeUnit.SECONDS), "child JVM hung"); + assertEquals(0, p.exitValue(), out); + assertTrue(out.contains("carrier parallelism = 1"), out); + assertTrue(out.contains("[odd:1, even:2, odd:3, even:4, odd:5, even:6, odd:7, even:8, odd:9, even:10]"), out); + assertTrue(out.contains("3000 events"), out); + } +}