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 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
@@ -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.
|
||||
@@ -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]
|
||||
@@ -0,0 +1,2 @@
|
||||
PingPong, 4 rounds
|
||||
[ping, pong, ping, pong, ping, pong, ping, pong]
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ankurm.interviewlab</groupId>
|
||||
<artifactId>multithreading</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>multithreading</name>
|
||||
<description>Classic multithreading interview programs: odd-even, ping-pong, ordered threads, deadlock</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>6.1.3</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Executable
+21
@@ -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
|
||||
Executable
+11
@@ -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'
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
+46
@@ -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<List<String>> 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<String> describe(long[] ids) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
+63
@@ -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<List<String>> deadlock, List<Thread.State> 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<List<String>> deadlock = Optional.empty();
|
||||
List<Thread.State> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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<Thread> threads = new ArrayList<>();
|
||||
private final List<Throwable> 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<Thread> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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<String> events = new ArrayList<>(); // guarded by lock
|
||||
|
||||
@Override
|
||||
public List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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<String> print(int max, ThreadKind kind) throws Exception;
|
||||
}
|
||||
+36
@@ -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<String> print(int max, ThreadKind kind) throws Exception {
|
||||
Semaphore oddTurn = new Semaphore(1); // odd goes first
|
||||
Semaphore evenTurn = new Semaphore(0);
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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<String> events = new ArrayList<>(); // guarded by monitor
|
||||
|
||||
@Override
|
||||
public List<String> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -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<String> withLatches(ThreadKind kind) throws Exception {
|
||||
List<String> 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<String> withJoin(ThreadKind kind) throws Exception {
|
||||
List<String> 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<String> roundRobin(int threadCount, int max, ThreadKind kind) throws Exception {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> events = new ArrayList<>(); // guarded by lock
|
||||
|
||||
public List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<Supplier<OddEvenPrinter>> IMPLEMENTATIONS = List.of(
|
||||
OddEvenWaitNotify::new, OddEvenSemaphore::new, OddEvenLockCondition::new);
|
||||
|
||||
static List<String> expected(int max) {
|
||||
List<String> 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<OddEvenPrinter> 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<OddEvenPrinter> 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<OddEvenPrinter> 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<String> 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);
|
||||
}
|
||||
}
|
||||
+44
@@ -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<String> 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<String> events = OrderedThreads.roundRobin(1000, 5000, ThreadKind.VIRTUAL);
|
||||
List<String> expected = IntStream.rangeClosed(1, 5000).mapToObj(n -> "worker-" + ((n - 1) % 1000 + 1) + ":" + n).toList();
|
||||
assertEquals(expected, events);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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));
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user