Files
asmhatre 32b8067ecf locks: synchronized vs ReentrantLock vs StampedLock companion code
JMH throughput sweep (1-64 threads), 9:1 read-heavy @Group benchmark,
tryLock(timeout) deadlock-avoidance demo, and a JDK21-vs-JDK25 JEP 491
virtual-thread-pinning comparison. Also promotes LICENSE to repo root
now that a second module exists.
2026-09-30 06:04:50 +00:00
..

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

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 - see the repo-wide LICENSE.