jmm: Java Memory Model companion code (volatile, happens-before, DCL)

Runnable jcstress + JUnit companion for the ankurm.com post on the
Java Memory Model, double-checked locking, and the volatile fix.
This commit is contained in:
2026-09-30 05:31:29 +00:00
commit f548d9eeb5
22 changed files with 721 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# jmm
Companion code for the ankurm.com post *"The Java Memory Model Explained: volatile,
happens-before, and Why Your Double-Checked Lock Failed."* First module in `java-core-examples`,
the Java-core / concurrency series.
## Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.4.1+1 (Temurin, LTS) | GA'd September 2025. `docker`/CI images still default to 21; this repo needs 25 on `PATH` or `JAVA_HOME`. |
| jcstress-core | 0.16 | Latest on Maven Central as of this writing (last published Feb 2023 - the tool is stable, not stale). |
| JUnit Jupiter | 5.11.0 | Sanity tests only - see the warning below. |
| Maven | 3.9.11 | |
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25 # must be 25, see the trap below
mvn package
java -jar jmm/target/jcstress.jar -t PlainPublicationTest -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 -v
```
`scripts/run-all.sh` regenerates every file in `output/` with the same commands used to
produce the ones committed here. `scripts/run.sh <TestClass>` runs one test ad hoc.
## What's in here
| File | What it shows |
|---|---|
| `src/main/java/.../BrokenDclSingleton.java` | The classic double-checked-locking bug: plain (non-volatile) `instance` field. |
| `src/main/java/.../VolatileDclSingleton.java` | The JSR-133 fix: mark `instance` `volatile`. |
| `src/main/java/.../HolderIdiomSingleton.java` | The idiom to actually use: initialization-on-demand holder, no `volatile` needed. |
| `src/main/java/.../Payload.java` / `FinalPayload.java` | The two payload shapes the jcstress tests publish - plain fields vs. `final` fields. |
| `src/main/java/.../PlainPublicationTest.java` | jcstress test reproducing `BrokenDclSingleton`'s unsynchronized publish. |
| `src/main/java/.../VolatilePublicationTest.java` | Same race, `volatile` reference field. |
| `src/main/java/.../FinalFieldPublicationTest.java` | Same race, plain reference field but `final` payload fields - tests the JLS 17.5 guarantee in isolation from `volatile`. |
| `src/test/java/.../SingletonBehaviorTest.java` | Ordinary JUnit sanity checks - does NOT prove the memory-visibility claims, see its Javadoc. |
| `output/01` - `output/03` | Captured jcstress runs for the three tests above. |
| `output/04` | A real trap hit building this repo: JDK 25 silently stops discovering annotation processors on the plain classpath. |
| `output/05` | The JUnit sanity run. |
## The jcstress tests did not reproduce the bug on this hardware, and that's reported honestly
All three jcstress campaigns (`output/01`-`03`) ran to completion with **zero** occurrences of
the torn/reordered outcome, across roughly 5.2, 5.3, and 5.6 billion sampled interleavings
respectively. That is expected, not a negative result:
- x86-64's TSO memory model does not permit store-store reordering in hardware, which is half
of what the classic "fails on ARM" claim depends on.
- This sandbox is a 2-vCPU x86-64 VM. The historically-reported failures on real hardware are
on weak-memory architectures (ARM, POWER) or come from compiler-level store reordering, which
is possible on any architecture but was not observed here in ~16 billion combined samples.
**This repository does not claim to have reproduced the bug on ARM hardware** - there was none
available to test on. The ARM claim in the post is attributed to the documented JSR-133
rationale and widely-reported real-world failures (cited in the post's further-reading list),
not to a run captured in this repo. What jcstress here does prove, directly and reproducibly:
the unsynchronized version has two outcomes jcstress classifies as merely `Interesting` rather
than ruled out, while the `volatile` and `final`-field versions have the same outcomes marked
`Forbidden` and jcstress would report a **hard error** if it ever saw one. It did not, in either
of those two, which is the actual evidence that both fixes work.
## Traps hit building this
- **jcstress tests must live in `src/main/java`, not `src/test/java`.** `maven-shade-plugin`
only shades the main artifact; test-classes never make it into the runnable jar, so
`META-INF/TestList` ends up empty and `jcstress.jar` throws a `NullPointerException` in
`TestList.getTests()` on startup with no test classes found. (Costs a rebuild if you don't
know to look for it - see the class-count check in `scripts/run-all.sh`'s comments.)
- **JDK 25 needs `<annotationProcessorPaths>` explicitly.** JDK 21 still discovers annotation
processors on the plain classpath, with a warning that this is deprecated
(`output/04-annotation-processing-jdk21-vs-jdk25.txt` has the exact message). On JDK 25 that
discovery is simply gone - no warning, no error, the `*_jcstress.java` harness classes are
never generated and the build "succeeds" with nothing to run. Verified directly with `javac`
on both JDKs while building this repo.
- **`jcstress-maven-plugin` does not exist.** The dependency is just `jcstress-core` plus
`maven-shade-plugin` with `org.openjdk.jcstress.Main` as the shaded manifest's main class.
(The Maven coordinate that *does* exist is `jcstress-java-test-archetype`, for scaffolding a
fresh project via `mvn archetype:generate` - not needed once you have a working pom.)
- **jcstress's result classes are not `IntResult2`/`IntResult3`.** They're named by type-code,
e.g. two ints is `org.openjdk.jcstress.infra.results.II_Result` (fields `r1`, `r2`).
- **Auto-detected JVM configs multiply fork counts fast.** Without `-jvmArgs`, jcstress probes
and iterates several compiler-flag combinations per fork, which is fine on real hardware but
turned a "quick" run into an 8+ minute one on this sandbox's 2 CPUs. Passing `-jvmArgs
"-Xmx256m"` (or anything) forces single-JVM-config mode and makes run time predictable.
- **`UseBiasedLocking` probe fails loudly but harmlessly.** jcstress 0.16 (2023) still probes for
biased locking, which JEP 374 removed in JDK 15. jcstress marks the probe `[N/A]` and moves on;
it is not a build blocker.
## License
MIT.