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
79 lines
4.7 KiB
Markdown
79 lines
4.7 KiB
Markdown
# 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).
|