# 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 ` 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 `` 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 - see the [repo-wide LICENSE](../LICENSE).