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
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user