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