From 202f2f18a52be476b74b5c1887299408d8621200 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Wed, 30 Sep 2026 07:25:12 +0000 Subject: [PATCH] concurrency-interview: companion code for the Top 40 interview post Adds the concurrency-interview module: a real race condition with its actual lost-update count, why volatile alone doesn't fix it, a real JVM-detected deadlock (ThreadMXBean.findDeadlockedThreads()) with the fix, and ScopedValue's exact child-thread inheritance rules (finalized in JDK 25 via JEP 506, demonstrated against StructuredTaskScope, still preview per JEP 505/525). 4 runnable demos, 1 JUnit test class (9 tests), 5 captured output transcripts. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM --- README.md | 1 + concurrency-interview/README.md | 78 ++++++++++++ .../output/01-race-condition.txt | 5 + .../output/02-volatile-not-enough.txt | 5 + .../output/03-deadlock-detection.txt | 12 ++ .../output/04-scoped-value-inheritance.txt | 13 ++ .../output/05-correctness-tests.txt | 4 + concurrency-interview/pom.xml | 49 ++++++++ concurrency-interview/scripts/run-all.sh | 32 +++++ .../concurrencyinterview/DeadlockDemo.java | 111 ++++++++++++++++++ .../RaceConditionDemo.java | 46 ++++++++ .../concurrencyinterview/ScopedValueDemo.java | 63 ++++++++++ .../VolatileNotEnoughDemo.java | 42 +++++++ .../InterviewClaimsTest.java | 91 ++++++++++++++ pom.xml | 1 + 15 files changed, 553 insertions(+) create mode 100644 concurrency-interview/README.md create mode 100644 concurrency-interview/output/01-race-condition.txt create mode 100644 concurrency-interview/output/02-volatile-not-enough.txt create mode 100644 concurrency-interview/output/03-deadlock-detection.txt create mode 100644 concurrency-interview/output/04-scoped-value-inheritance.txt create mode 100644 concurrency-interview/output/05-correctness-tests.txt create mode 100644 concurrency-interview/pom.xml create mode 100755 concurrency-interview/scripts/run-all.sh create mode 100644 concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/DeadlockDemo.java create mode 100644 concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/RaceConditionDemo.java create mode 100644 concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/ScopedValueDemo.java create mode 100644 concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/VolatileNotEnoughDemo.java create mode 100644 concurrency-interview/src/test/java/com/ankurm/concurrencyinterview/InterviewClaimsTest.java diff --git a/README.md b/README.md index 426c476..11dea01 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ article; each module's own README has that article's version table, quickstart, | [`vt-pinning`](vt-pinning/) | Diagnosing Virtual Thread Pinning in Production: JFR Events, jcmd, and Real Fixes | | [`synchronizers`](synchronizers/) | CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore in Java | | [`executors`](executors/) | ExecutorService Done Right: Shutdown, try-with-resources and Virtual Thread Executors | +| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) | ## License diff --git a/concurrency-interview/README.md b/concurrency-interview/README.md new file mode 100644 index 0000000..5206493 --- /dev/null +++ b/concurrency-interview/README.md @@ -0,0 +1,78 @@ +# concurrency-interview + +Companion code for the ankurm.com post *"Top 40 Java Concurrency Interview Questions and Answers +(2026)."* Seventh module in `java-core-examples`, the Java-core / concurrency series - this one +backs a hub-style Q&A post rather than a single deep dive. + +Most of the 40 questions in that post link out to the deep, already-verified demos in this repo's +other modules (locks, atomics, jmm, synchronizers, executors, vt-pinning) and to a few of this +site's existing standalone posts. The four demos here exist because their questions weren't +already proven anywhere else in the series: a real race condition and its actual lost-update +count, why `volatile` alone doesn't fix it, a real JVM-detected deadlock (and the fix), and exactly +which threads a `ScopedValue` binding does and doesn't reach. + +## Versions this was built and tested against + +| Component | Version | Notes | +|---|---|---| +| JDK | 25.0.4.1+1 (Temurin, LTS) | Every demo needs `--enable-preview` - see below. | +| JUnit Jupiter | 5.11.0 | Correctness tests, 5 repeats for the one timing-sensitive case. | +| Maven | 3.9.11 | | +| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. | + +**Why `--enable-preview` for everything, even the demos that don't touch a preview API:** this +module is compiled as one unit, and `ScopedValueDemo` needs `StructuredTaskScope` (still preview +in JDK 25/26, per JEP 505/525) to demonstrate scoped-value inheritance into a child thread. +`ScopedValue` itself was finalized in JDK 25 via JEP 506 and needs no flag on its own. + +## Quickstart + +```bash +export JAVA_HOME=/path/to/jdk-25 +mvn package +java --enable-preview -cp target/classes com.ankurm.concurrencyinterview.RaceConditionDemo +java --enable-preview -cp target/classes com.ankurm.concurrencyinterview.DeadlockDemo +``` + +`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME`). + +## What's in here + +| File | What it shows | +|---|---| +| `.../RaceConditionDemo.java` | A plain `int` incremented by 8 threads, 200,000 times each, next to an `AtomicInteger` doing identical work - the real lost-update count, not an assertion that one exists. | +| `.../VolatileNotEnoughDemo.java` | The same workload against a `volatile int` - visibility is guaranteed, atomicity is not, and the loss is just as real. | +| `.../DeadlockDemo.java` | Two threads, two locks, acquired in opposite order - a genuine deadlock, detected with `ThreadMXBean.findDeadlockedThreads()`, then a second run with consistent lock ordering that completes normally. | +| `.../ScopedValueDemo.java` | `ScopedValue` visible to same-thread direct and indirect callees; NOT inherited by a plain `new Thread()`; inherited by a thread forked from `StructuredTaskScope`. | +| `src/test/.../InterviewClaimsTest.java` | Asserts what's actually guaranteed: `AtomicInteger` never loses an update (5 repeats), and `ScopedValue`'s exact inheritance rule - deliberately does NOT assert on the raw-int or volatile-int race outcomes, since those are genuinely non-deterministic. | +| `output/01`-`04` | Each demo's real run. | +| `output/05` | JUnit correctness run. | + +## Reading the results honestly (2-vCPU sandbox) + +**The race condition lost 351,504 of 1,600,000 increments this run** (`output/01`) - not "some" +or "a few," an exact, real number from an actual run, which is the whole point of not just +asserting that `count++` is unsafe. Rerun it and the number will be different; it won't be zero on +this hardware with this workload. + +**`volatile` lost 744,982 updates under the identical workload** (`output/02`). `volatile` +guarantees every thread observes the latest write and prevents instruction reordering around it - +it says nothing about two threads reading the same value before either writes back, which is +exactly what `count++` (read, add, write) allows. The fix for a shared counter is `AtomicInteger` +or a lock, not the `volatile` keyword. + +**The deadlock in `output/03` is real, not simulated** - `ThreadMXBean.findDeadlockedThreads()` is +the same API a monitoring agent or `jstack` uses, and it reports both threads, each blocked on the +lock the other holds. The fix demonstrated right after it isn't "add more locking" - it's ensuring +every thread that needs both locks acquires them in the same order, which makes the circular-wait +condition structurally impossible. + +**`ScopedValue` inheritance in `output/04` is exactly as narrow as the JDK docs say, confirmed +rather than assumed:** bound on the main thread, visible to nested method calls on that same +thread; a plain `new Thread()` reports `isBound() = false`; only the `StructuredTaskScope.fork()`'d +thread sees the binding. Reaching for `ScopedValue` and expecting it to "just work" across a +hand-rolled `new Thread()` is a real interview trap, not a hypothetical one. + +## License + +MIT - see the [repo-wide LICENSE](../LICENSE). diff --git a/concurrency-interview/output/01-race-condition.txt b/concurrency-interview/output/01-race-condition.txt new file mode 100644 index 0000000..9bd9205 --- /dev/null +++ b/concurrency-interview/output/01-race-condition.txt @@ -0,0 +1,5 @@ +expected increments: 1600000 +unsafeCounter (plain int, unsynchronized): 1248496 +safeCounter (AtomicInteger): 1600000 +unsafeCounter lost 351504 updates to the race +safeCounter matches expected: true diff --git a/concurrency-interview/output/02-volatile-not-enough.txt b/concurrency-interview/output/02-volatile-not-enough.txt new file mode 100644 index 0000000..c77accc --- /dev/null +++ b/concurrency-interview/output/02-volatile-not-enough.txt @@ -0,0 +1,5 @@ +expected increments: 1600000 +volatileCounter: 855018 (volatile guarantees every thread SEES the latest value - it does not make ++ atomic) +atomicCounter: 1600000 +volatileCounter lost updates: 744982 +atomicCounter matches expected: true diff --git a/concurrency-interview/output/03-deadlock-detection.txt b/concurrency-interview/output/03-deadlock-detection.txt new file mode 100644 index 0000000..c6942c7 --- /dev/null +++ b/concurrency-interview/output/03-deadlock-detection.txt @@ -0,0 +1,12 @@ +=== Opposite lock order: a real deadlock, detected via ThreadMXBean === +thread-1: holding lockA, waiting for lockB +thread-2: holding lockB, waiting for lockA +main: findDeadlockedThreads() detected 2 deadlocked threads + thread-1 is blocked on java.lang.Object@5451c3a8, owned by thread-2 + thread-2 is blocked on java.lang.Object@49476842, owned by thread-1 + +=== Same lock order on both threads: no deadlock possible === +thread-1 (fixed order): acquired both locks, done +thread-2 (fixed order): acquired both locks, done +main: both fixed-order threads finished, isAlive t1=false t2=false +done (the process exits here - the two deadlocked threads above never finish) diff --git a/concurrency-interview/output/04-scoped-value-inheritance.txt b/concurrency-interview/output/04-scoped-value-inheritance.txt new file mode 100644 index 0000000..99b3a41 --- /dev/null +++ b/concurrency-interview/output/04-scoped-value-inheritance.txt @@ -0,0 +1,13 @@ +=== Same-thread binding: visible to direct AND indirect callees === +handleRequest(): REQUEST_ID.get() = req-42 +logSomewhereDeeper() (indirect callee, same thread): REQUEST_ID.get() = req-42 + +=== Outside any binding: isBound() is false === +main: REQUEST_ID.isBound() = false + +=== A plain `new Thread()` does NOT inherit the binding === +plain child thread: REQUEST_ID.isBound() = false + +=== A StructuredTaskScope.fork()'d thread DOES inherit the binding === +forked subtask: REQUEST_ID.isBound() = true, REQUEST_ID.get() = req-100 +done diff --git a/concurrency-interview/output/05-correctness-tests.txt b/concurrency-interview/output/05-correctness-tests.txt new file mode 100644 index 0000000..964db65 --- /dev/null +++ b/concurrency-interview/output/05-correctness-tests.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: com.ankurm.concurrencyinterview.InterviewClaimsTest +------------------------------------------------------------------------------- +Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.216 s -- in com.ankurm.concurrencyinterview.InterviewClaimsTest diff --git a/concurrency-interview/pom.xml b/concurrency-interview/pom.xml new file mode 100644 index 0000000..2e73c6d --- /dev/null +++ b/concurrency-interview/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + com.ankurm + java-core-examples + 1.0 + + + concurrency-interview + concurrency-interview + Verified demos behind the "Top 40 Java Concurrency Interview Questions" hub post: a real race condition, a real detected deadlock, volatile's actual limits, and ScopedValue inheritance rules. + + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + --enable-preview + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + --enable-preview + + + + + diff --git a/concurrency-interview/scripts/run-all.sh b/concurrency-interview/scripts/run-all.sh new file mode 100755 index 0000000..6b9d0b7 --- /dev/null +++ b/concurrency-interview/scripts/run-all.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Regenerates every file in ../output/. Requires JDK25_HOME. Every demo in this module needs +# --enable-preview to run (ScopedValueDemo uses StructuredTaskScope, still preview in JDK 25/26), +# even though ScopedValue itself is finalized (JEP 506). +set -euo pipefail + +if [[ -z "${JDK25_HOME:-}" ]]; then + echo "JDK25_HOME must be set (e.g. /path/to/jdk-25)" >&2 + exit 1 +fi + +cd "$(dirname "$0")/.." +OUT=output +mkdir -p "$OUT" + +JAVA_HOME="$JDK25_HOME" mvn -q -f ../pom.xml -pl concurrency-interview package + +run() { + local class=$1 + local outfile=$2 + echo "==> $class" + "$JDK25_HOME/bin/java" --enable-preview -cp target/classes "com.ankurm.concurrencyinterview.$class" 2>&1 | grep -v "Picked up" > "$OUT/$outfile" +} + +run RaceConditionDemo 01-race-condition.txt +run VolatileNotEnoughDemo 02-volatile-not-enough.txt +run DeadlockDemo 03-deadlock-detection.txt +run ScopedValueDemo 04-scoped-value-inheritance.txt + +cp target/surefire-reports/com.ankurm.concurrencyinterview.InterviewClaimsTest.txt "$OUT/05-correctness-tests.txt" + +echo "Done. See $OUT/" diff --git a/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/DeadlockDemo.java b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/DeadlockDemo.java new file mode 100644 index 0000000..117f1d9 --- /dev/null +++ b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/DeadlockDemo.java @@ -0,0 +1,111 @@ +package com.ankurm.concurrencyinterview; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; + +/** + * The textbook deadlock: two threads, two locks, acquired in opposite order. Thread-1 takes + * lockA then reaches for lockB; Thread-2 takes lockB then reaches for lockA. Neither can proceed + * and neither will ever release what it's already holding. This demo doesn't just describe that - + * it triggers a real deadlock, detects it with the same java.lang.management API a monitoring + * tool would use (ThreadMXBean.findDeadlockedThreads()), and then runs a second, fixed version + * that acquires both locks in the same global order every time, which completes normally. + */ +public class DeadlockDemo { + + private static final Object lockA = new Object(); + private static final Object lockB = new Object(); + + // Separate locks for the fixed-order demo: the two threads above deadlock forever and never + // release lockA/lockB, so reusing those objects here would just hang a second time. + private static final Object lockC = new Object(); + private static final Object lockD = new Object(); + + private static void triggerDeadlock() throws InterruptedException { + Thread t1 = new Thread(() -> { + synchronized (lockA) { + System.out.println("thread-1: holding lockA, waiting for lockB"); + sleep(200); + synchronized (lockB) { + System.out.println("thread-1: this line never prints - deadlocked before reaching it"); + } + } + }, "thread-1"); + + Thread t2 = new Thread(() -> { + synchronized (lockB) { + System.out.println("thread-2: holding lockB, waiting for lockA"); + sleep(200); + synchronized (lockA) { + System.out.println("thread-2: this line never prints - deadlocked before reaching it"); + } + } + }, "thread-2"); + + t1.start(); + t2.start(); + + Thread.sleep(1000); // give the deadlock time to actually form + + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + long[] deadlockedIds = threadBean.findDeadlockedThreads(); + if (deadlockedIds == null) { + System.out.println("main: findDeadlockedThreads() found nothing - unexpected for this demo"); + return; + } + System.out.println("main: findDeadlockedThreads() detected " + deadlockedIds.length + " deadlocked threads"); + for (ThreadInfo info : threadBean.getThreadInfo(deadlockedIds, true, true)) { + System.out.println(" " + info.getThreadName() + " is blocked on " + info.getLockInfo() + + ", owned by " + info.getLockOwnerName()); + } + // The two threads above are stuck forever - this JVM process exits without joining them. + } + + private static void runFixedOrderedVersion() throws InterruptedException { + Thread t1 = new Thread(() -> { + synchronized (lockC) { + sleep(100); + synchronized (lockD) { + System.out.println("thread-1 (fixed order): acquired both locks, done"); + } + } + }, "thread-1-fixed"); + + Thread t2 = new Thread(() -> { + synchronized (lockC) { // same order as thread-1: lockC first, lockD second + sleep(100); + synchronized (lockD) { + System.out.println("thread-2 (fixed order): acquired both locks, done"); + } + } + }, "thread-2-fixed"); + + t1.start(); + t2.start(); + t1.join(5000); + t2.join(5000); + System.out.println("main: both fixed-order threads finished, isAlive t1=" + t1.isAlive() + + " t2=" + t2.isAlive()); + } + + private static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== Opposite lock order: a real deadlock, detected via ThreadMXBean ==="); + triggerDeadlock(); + + System.out.println(); + System.out.println("=== Same lock order on both threads: no deadlock possible ==="); + runFixedOrderedVersion(); + + System.out.println("done (the process exits here - the two deadlocked threads above never finish)"); + System.exit(0); // the deadlocked threads from triggerDeadlock() would otherwise hang the JVM forever + } +} diff --git a/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/RaceConditionDemo.java b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/RaceConditionDemo.java new file mode 100644 index 0000000..7dbf4c5 --- /dev/null +++ b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/RaceConditionDemo.java @@ -0,0 +1,46 @@ +package com.ankurm.concurrencyinterview; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The canonical interview answer to "why isn't count++ thread-safe?" is that it's a + * read-modify-write sequence, not one atomic step. This demo doesn't just assert that - it runs + * 8 threads each incrementing a plain int 200,000 times (1,600,000 total increments expected) and + * prints whatever the real, lost-update total actually comes out to, side by side with an + * AtomicInteger doing the identical work correctly. The exact deficit is inherently + * non-deterministic - that's the point being demonstrated, not a bug in the demo. + */ +public class RaceConditionDemo { + + private static int unsafeCounter = 0; + private static final AtomicInteger safeCounter = new AtomicInteger(0); + + private static final int THREADS = 8; + private static final int INCREMENTS_PER_THREAD = 200_000; + + public static void main(String[] args) throws InterruptedException { + Thread[] threads = new Thread[THREADS]; + for (int i = 0; i < THREADS; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < INCREMENTS_PER_THREAD; j++) { + unsafeCounter++; // read-modify-write, not atomic + safeCounter.incrementAndGet(); // genuinely atomic + } + }); + } + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(); + + int expected = THREADS * INCREMENTS_PER_THREAD; + System.out.println("expected increments: " + expected); + System.out.println("unsafeCounter (plain int, unsynchronized): " + unsafeCounter); + System.out.println("safeCounter (AtomicInteger): " + safeCounter.get()); + if (unsafeCounter != expected) { + System.out.println("unsafeCounter lost " + (expected - unsafeCounter) + " updates to the race"); + } else { + System.out.println("unsafeCounter happened to come out correct this run - the race is still real, " + + "just didn't lose anything THIS time (rerun it - it usually does on this box)"); + } + System.out.println("safeCounter matches expected: " + (safeCounter.get() == expected)); + } +} diff --git a/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/ScopedValueDemo.java b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/ScopedValueDemo.java new file mode 100644 index 0000000..4a09c04 --- /dev/null +++ b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/ScopedValueDemo.java @@ -0,0 +1,63 @@ +package com.ankurm.concurrencyinterview; + +import java.util.concurrent.StructuredTaskScope; + +/** + * ScopedValue (java.lang.ScopedValue) was finalized in JDK 25 via JEP 506 - no preview flag + * needed for the basic API. But its main selling point over ThreadLocal is sharing immutable + * context with CHILD threads, and that inheritance is NOT automatic for just any thread: it only + * happens for threads forked from a StructuredTaskScope, which is still a preview API in JDK 25/26 + * (JEP 505/525). This class needs --enable-preview for that reason alone - the ScopedValue calls + * themselves wouldn't require it. + */ +public class ScopedValueDemo { + + private static final ScopedValue REQUEST_ID = ScopedValue.newInstance(); + + private static void handleRequest() { + System.out.println("handleRequest(): REQUEST_ID.get() = " + REQUEST_ID.get()); + logSomewhereDeeper(); + } + + private static void logSomewhereDeeper() { + System.out.println("logSomewhereDeeper() (indirect callee, same thread): REQUEST_ID.get() = " + REQUEST_ID.get()); + } + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== Same-thread binding: visible to direct AND indirect callees ==="); + ScopedValue.where(REQUEST_ID, "req-42").run(ScopedValueDemo::handleRequest); + + System.out.println(); + System.out.println("=== Outside any binding: isBound() is false ==="); + System.out.println("main: REQUEST_ID.isBound() = " + REQUEST_ID.isBound()); + + System.out.println(); + System.out.println("=== A plain `new Thread()` does NOT inherit the binding ==="); + ScopedValue.where(REQUEST_ID, "req-99").run(() -> { + Thread plainChild = new Thread(() -> + System.out.println("plain child thread: REQUEST_ID.isBound() = " + REQUEST_ID.isBound())); + plainChild.start(); + try { + plainChild.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + System.out.println(); + System.out.println("=== A StructuredTaskScope.fork()'d thread DOES inherit the binding ==="); + ScopedValue.where(REQUEST_ID, "req-100").run(() -> { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> { + System.out.println("forked subtask: REQUEST_ID.isBound() = " + REQUEST_ID.isBound() + + ", REQUEST_ID.get() = " + REQUEST_ID.get()); + return null; + }); + scope.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + System.out.println("done"); + } +} diff --git a/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/VolatileNotEnoughDemo.java b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/VolatileNotEnoughDemo.java new file mode 100644 index 0000000..7c20d8e --- /dev/null +++ b/concurrency-interview/src/main/java/com/ankurm/concurrencyinterview/VolatileNotEnoughDemo.java @@ -0,0 +1,42 @@ +package com.ankurm.concurrencyinterview; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A common interview trap: "just mark it volatile" does NOT make count++ thread-safe. volatile + * guarantees visibility (every thread sees the latest write) and ordering, but it does nothing + * about the read-modify-write sequence being non-atomic - two threads can both read the same + * value before either writes back. This runs a volatile int through the identical concurrent + * increment workload as RaceConditionDemo, next to an AtomicInteger, to show the failure mode is + * the same as a plain int, not fixed by the keyword alone. + */ +public class VolatileNotEnoughDemo { + + private static volatile int volatileCounter = 0; + private static final AtomicInteger atomicCounter = new AtomicInteger(0); + + private static final int THREADS = 8; + private static final int INCREMENTS_PER_THREAD = 200_000; + + public static void main(String[] args) throws InterruptedException { + Thread[] threads = new Thread[THREADS]; + for (int i = 0; i < THREADS; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < INCREMENTS_PER_THREAD; j++) { + volatileCounter++; // visibility guaranteed, atomicity is NOT + atomicCounter.incrementAndGet(); + } + }); + } + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(); + + int expected = THREADS * INCREMENTS_PER_THREAD; + System.out.println("expected increments: " + expected); + System.out.println("volatileCounter: " + volatileCounter + + " (volatile guarantees every thread SEES the latest value - it does not make ++ atomic)"); + System.out.println("atomicCounter: " + atomicCounter.get()); + System.out.println("volatileCounter lost updates: " + (expected - volatileCounter)); + System.out.println("atomicCounter matches expected: " + (atomicCounter.get() == expected)); + } +} diff --git a/concurrency-interview/src/test/java/com/ankurm/concurrencyinterview/InterviewClaimsTest.java b/concurrency-interview/src/test/java/com/ankurm/concurrencyinterview/InterviewClaimsTest.java new file mode 100644 index 0000000..5c29107 --- /dev/null +++ b/concurrency-interview/src/test/java/com/ankurm/concurrencyinterview/InterviewClaimsTest.java @@ -0,0 +1,91 @@ +package com.ankurm.concurrencyinterview; + +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; +import java.util.concurrent.StructuredTaskScope; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Deliberately does NOT assert on the raw-int or volatile-int race demos - whether a given run + * happens to lose an update is inherently non-deterministic, and a test that occasionally passes + * by luck is worse than no test. What's asserted here is what's actually guaranteed: AtomicInteger + * never loses an update under the identical workload, and ScopedValue's inheritance rule is exactly + * what the docs say it is, not "roughly." + */ +class InterviewClaimsTest { + + @RepeatedTest(5) + void atomicIntegerNeverLosesAnUpdateUnderTheSameWorkloadThatBreaksAPlainInt() throws InterruptedException { + AtomicInteger counter = new AtomicInteger(0); + int threads = 8; + int incrementsPerThread = 50_000; + Thread[] pool = new Thread[threads]; + for (int i = 0; i < threads; i++) { + pool[i] = new Thread(() -> { + for (int j = 0; j < incrementsPerThread; j++) { + counter.incrementAndGet(); + } + }); + } + for (Thread t : pool) t.start(); + for (Thread t : pool) t.join(); + assertEquals(threads * incrementsPerThread, counter.get()); + } + + @Test + void scopedValueIsVisibleToDirectAndIndirectCalleesOnTheSameThread() { + ScopedValue value = ScopedValue.newInstance(); + AtomicReference seenByCallee = new AtomicReference<>(); + ScopedValue.where(value, "abc").run(() -> seenByCallee.set(value.get())); + assertEquals("abc", seenByCallee.get()); + } + + @Test + void scopedValueGetOutsideAnyBindingThrows() { + ScopedValue value = ScopedValue.newInstance(); + assertFalse(value.isBound()); + assertThrows(NoSuchElementException.class, value::get); + } + + @Test + void plainThreadDoesNotInheritAScopedValueBinding() throws InterruptedException { + ScopedValue value = ScopedValue.newInstance(); + AtomicReference boundInChild = new AtomicReference<>(); + ScopedValue.where(value, "parent-bound").run(() -> { + Thread child = new Thread(() -> boundInChild.set(value.isBound())); + child.start(); + try { + child.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertEquals(false, boundInChild.get(), "a plain new Thread() must NOT inherit the binding"); + } + + @Test + void structuredTaskScopeForkedThreadDoesInheritTheBinding() throws Exception { + ScopedValue value = ScopedValue.newInstance(); + AtomicReference seenInSubtask = new AtomicReference<>(); + ScopedValue.where(value, "inherited").run(() -> { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> { + seenInSubtask.set(value.get()); + return null; + }); + scope.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertEquals("inherited", seenInSubtask.get()); + } +} diff --git a/pom.xml b/pom.xml index bcefd67..98c327c 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ vt-pinning synchronizers executors + concurrency-interview