Files
asmhatreandClaude Sonnet 5 202f2f18a5 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
2026-09-30 07:25:12 +00:00
..

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.