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 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
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
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.