commit f548d9eeb5a06ef3597bd8dcef6f96fc707a22c7 Author: asmhatre Date: Wed Sep 30 05:31:29 2026 +0000 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2f577b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target/ +*.class +jmm/jcstress-results-*.bin.gz +jmm/dependency-reduced-pom.xml +jmm/post.html +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..75ebe41 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# java-core-examples + +Runnable companion code for ankurm.com's Java-core / concurrency series. One Maven module per +article; each module's own README has that article's version table, quickstart, and traps. + +| Module | Post | +|---|---| +| [`jmm`](jmm/) | The Java Memory Model Explained: volatile, happens-before, and Why Your Double-Checked Lock Failed | + +## License + +MIT - see [LICENSE](jmm/LICENSE) (per-module; a repo-wide LICENSE will move here once a second +module needs it). diff --git a/jmm/LICENSE b/jmm/LICENSE new file mode 100644 index 0000000..aa5473f --- /dev/null +++ b/jmm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ankur Mhatre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/jmm/README.md b/jmm/README.md new file mode 100644 index 0000000..f3ccd59 --- /dev/null +++ b/jmm/README.md @@ -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 ` 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. diff --git a/jmm/output/01-plain-publication.txt b/jmm/output/01-plain-publication.txt new file mode 100644 index 0000000..5e3bee3 --- /dev/null +++ b/jmm/output/01-plain-publication.txt @@ -0,0 +1,21 @@ +$ java -jar target/jcstress.jar -t PlainPublicationTest -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 + +RUN RESULTS: + Interesting tests: No matches. + + Failed tests: No matches. + + Error tests: No matches. + + All remaining tests: 1 matching test results. + +.......... [OK] com.ankurm.jmm.PlainPublicationTest + + Results across all configurations: + + RESULT SAMPLES FREQ EXPECT DESCRIPTION + 0, 0 3,511,549,619 66.79% Acceptable Reader ran before the write was published at all. + 0, 1 0 0.00% Interesting Reordering: the reference was visible before both of its ... + 1, 0 0 0.00% Interesting Reordering: the reference was visible before both of its ... + 1, 1 1,745,742,499 33.21% Acceptable Reader saw the fully-constructed Payload. + diff --git a/jmm/output/02-volatile-publication.txt b/jmm/output/02-volatile-publication.txt new file mode 100644 index 0000000..21d0127 --- /dev/null +++ b/jmm/output/02-volatile-publication.txt @@ -0,0 +1,21 @@ +$ java -jar target/jcstress.jar -t VolatilePublicationTest -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 + +RUN RESULTS: + Interesting tests: No matches. + + Failed tests: No matches. + + Error tests: No matches. + + All remaining tests: 1 matching test results. + +.......... [OK] com.ankurm.jmm.VolatilePublicationTest + + Results across all configurations: + + RESULT SAMPLES FREQ EXPECT DESCRIPTION + 0, 0 4,290,607,784 80.26% Acceptable Reader ran before the write was published at all. + 0, 1 0 0.00% Forbidden Would mean the volatile happens-before edge failed to hold. + 1, 0 0 0.00% Forbidden Would mean the volatile happens-before edge failed to hold. + 1, 1 1,055,076,014 19.74% Acceptable Reader saw the fully-constructed Payload. + diff --git a/jmm/output/03-final-field-publication.txt b/jmm/output/03-final-field-publication.txt new file mode 100644 index 0000000..9ccff15 --- /dev/null +++ b/jmm/output/03-final-field-publication.txt @@ -0,0 +1,21 @@ +$ java -jar target/jcstress.jar -t FinalFieldPublicationTest -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 + +RUN RESULTS: + Interesting tests: No matches. + + Failed tests: No matches. + + Error tests: No matches. + + All remaining tests: 1 matching test results. + +.......... [OK] com.ankurm.jmm.FinalFieldPublicationTest + + Results across all configurations: + + RESULT SAMPLES FREQ EXPECT DESCRIPTION + 0, 0 3,697,560,129 66.13% Acceptable Reader ran before the write was published at all. + 0, 1 0 0.00% Forbidden Would mean the final-field safe-publication guarantee fai... + 1, 0 0 0.00% Forbidden Would mean the final-field safe-publication guarantee fai... + 1, 1 1,893,699,349 33.87% Acceptable Reader saw the fully-constructed FinalPayload. + diff --git a/jmm/output/04-annotation-processing-jdk21-vs-jdk25.txt b/jmm/output/04-annotation-processing-jdk21-vs-jdk25.txt new file mode 100644 index 0000000..1eed6f3 --- /dev/null +++ b/jmm/output/04-annotation-processing-jdk21-vs-jdk25.txt @@ -0,0 +1,19 @@ +$ javac -cp jcstress-core.jar -d out -s gen PlainPublicationTest.java Payload.java # JDK 21.0.10, no -processorpath + +Note: Annotation processing is enabled because one or more processors were found + on the class path. A future release of javac may disable annotation processing + unless at least one processor is specified by name (-processor), or a search + path is specified (--processor-path, --processor-module-path), or annotation + processing is enabled explicitly (-proc:only, -proc:full). + Use -Xlint:-options to suppress this message. + Use -proc:none to disable annotation processing. + +$ find gen -type f +gen/com/ankurm/jmm/PlainPublicationTest_jcstress.java + +$ javac -cp jcstress-core.jar -d out -s gen PlainPublicationTest.java Payload.java # JDK 25.0.4.1+1, no -processorpath + +(no output at all - exit code 0) + +$ find gen -type f +(nothing - the processor was never invoked, no error, no warning) diff --git a/jmm/output/05-singleton-behavior.txt b/jmm/output/05-singleton-behavior.txt new file mode 100644 index 0000000..1170a29 --- /dev/null +++ b/jmm/output/05-singleton-behavior.txt @@ -0,0 +1,6 @@ +$ mvn -q test -pl jmm + +------------------------------------------------------------------------------- +Test set: com.ankurm.jmm.SingletonBehaviorTest +------------------------------------------------------------------------------- +Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.067 s -- in com.ankurm.jmm.SingletonBehaviorTest diff --git a/jmm/pom.xml b/jmm/pom.xml new file mode 100644 index 0000000..f9740c6 --- /dev/null +++ b/jmm/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + com.ankurm + java-core-examples + 1.0 + + + jmm + jmm + The Java Memory Model: volatile, happens-before, safe publication, and the double-checked-locking bug, reproduced with jcstress. + + + 0.16 + + + + + org.openjdk.jcstress + jcstress-core + ${jcstress.version} + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + + + jcstress + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + + + org.openjdk.jcstress + jcstress-core + ${jcstress.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + + + org.openjdk.jcstress.Main + + + + + + + + + diff --git a/jmm/scripts/run-all.sh b/jmm/scripts/run-all.sh new file mode 100755 index 0000000..93ecab1 --- /dev/null +++ b/jmm/scripts/run-all.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Regenerates every file in output/. Run from the jmm/ directory after `mvn package`. +# +# Needs a JDK 25 on PATH (or set JAVA_HOME below) - jcstress-core 0.16's annotation +# processor is NOT auto-discovered by javac on JDK 25 without -processorpath, which is +# why the pom's block exists; see output/04-*.txt. +# +# The three jcstress runs below use a fixed, non-default JVM config (-jvmArgs) on +# purpose: without it, jcstress auto-detects and iterates over several compiler-flag +# combinations, multiplying the number of forked JVMs far past what this sandbox's +# 2 CPUs can get through in a reasonable time. -f 2 -iters 15 -time 1000 with a fixed +# JVM config took about 4m45s per test on that hardware. +set -euo pipefail +cd "$(dirname "$0")/.." + +JAR=target/jcstress.jar +[ -f "$JAR" ] || { echo "Run 'mvn package' first."; exit 1; } + +run_test() { + local test_class="$1" out_file="$2" + echo "==> $test_class" + { + echo "\$ java -jar target/jcstress.jar -t $test_class -jvmArgs \"-Xmx256m\" -f 2 -iters 15 -time 1000" + echo + } > "$out_file" + java -jar "$JAR" -t "$test_class" -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 -v 2>/dev/null \ + | awk '/^RUN RESULTS:/{p=1} p==1 && /VM error stream:/{exit} p' >> "$out_file" +} + +run_test PlainPublicationTest output/01-plain-publication.txt +run_test VolatilePublicationTest output/02-volatile-publication.txt +run_test FinalFieldPublicationTest output/03-final-field-publication.txt + +echo "==> SingletonBehaviorTest (JUnit)" +mvn -q test +{ + echo '$ mvn -q test' + echo + cat target/surefire-reports/com.ankurm.jmm.SingletonBehaviorTest.txt +} > output/05-singleton-behavior.txt + +echo "Done. output/ regenerated." diff --git a/jmm/scripts/run.sh b/jmm/scripts/run.sh new file mode 100755 index 0000000..0c4446f --- /dev/null +++ b/jmm/scripts/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Quick ad hoc jcstress run against one test class, printed to the terminal instead of +# a captured output/ file. Example: +# ./scripts/run.sh PlainPublicationTest +# ./scripts/run.sh PlainPublicationTest -m tough # extra args pass straight through +set -euo pipefail +cd "$(dirname "$0")/.." + +TEST="${1:?Usage: run.sh [extra jcstress args]}" +shift || true + +JAR=target/jcstress.jar +[ -f "$JAR" ] || { echo "Run 'mvn package' first."; exit 1; } + +java -jar "$JAR" -t "$TEST" -jvmArgs "-Xmx256m" -f 2 -iters 15 -time 1000 -v "$@" diff --git a/jmm/src/main/java/com/ankurm/jmm/BrokenDclSingleton.java b/jmm/src/main/java/com/ankurm/jmm/BrokenDclSingleton.java new file mode 100644 index 0000000..8b0fb91 --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/BrokenDclSingleton.java @@ -0,0 +1,33 @@ +package com.ankurm.jmm; + +/** + * The classic broken double-checked locking idiom, exactly as it circulated in textbooks and + * interview prep before JSR-133 (2004) — and exactly as it still turns up in code review today. + * + *

{@code instance} is a plain field. The outer {@code if (instance == null)} check + * runs with no lock at all, so a second thread can read {@code instance} while the first thread + * is still inside the synchronized block, part-way through constructing the object. Whether + * that second thread then sees a fully-built {@link Payload} or a partially-built one is exactly + * the question {@code jcstress} answers in {@code PlainPublicationTest} — see + * {@code jmm/src/test/java/com/ankurm/jmm/PlainPublicationTest.java}, and the captured run in + * {@code output/01-plain-publication.txt}. + * + *

Do not copy this class into real code. It is here to fail. + */ +public final class BrokenDclSingleton { + private static Payload instance; + + private BrokenDclSingleton() { + } + + public static Payload getInstance() { + if (instance == null) { // 1st check, no lock + synchronized (BrokenDclSingleton.class) { + if (instance == null) { // 2nd check, under lock + instance = new Payload(1, 1); // <-- the unsynchronized publish + } + } + } + return instance; + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/FinalFieldPublicationTest.java b/jmm/src/main/java/com/ankurm/jmm/FinalFieldPublicationTest.java new file mode 100644 index 0000000..abe198b --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/FinalFieldPublicationTest.java @@ -0,0 +1,46 @@ +package com.ankurm.jmm; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.II_Result; + +import static org.openjdk.jcstress.annotations.Expect.*; + +/** + * The other way to make this safe, without touching the reference field at all: make the + * payload's own fields {@code final}, as {@link FinalPayload} and {@link HolderIdiomSingleton} + * do. The reference field below is deliberately plain, not volatile — the claim under test is + * that the JLS's final-field guarantee (17.5) alone is enough, regardless of how racy the + * reference read is. + * + *

Run: {@code java -jar target/jcstress.jar -t FinalFieldPublicationTest -v}
+ * Captured run: {@code output/03-final-field-publication.txt} + */ +@JCStressTest +@Outcome(id = "1, 1", expect = ACCEPTABLE, + desc = "Reader saw the fully-constructed FinalPayload.") +@Outcome(id = "0, 0", expect = ACCEPTABLE, + desc = "Reader ran before the write was published at all.") +@Outcome(id = {"1, 0", "0, 1"}, expect = FORBIDDEN, + desc = "Would mean the final-field safe-publication guarantee failed to hold.") +@State +public class FinalFieldPublicationTest { + + FinalPayload instance; + + @Actor + public void writer() { + instance = new FinalPayload(1, 1); + } + + @Actor + public void reader(II_Result r) { + FinalPayload p = instance; + if (p == null) { + r.r1 = 0; + r.r2 = 0; + } else { + r.r1 = p.a; + r.r2 = p.b; + } + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/FinalPayload.java b/jmm/src/main/java/com/ankurm/jmm/FinalPayload.java new file mode 100644 index 0000000..cb034dc --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/FinalPayload.java @@ -0,0 +1,23 @@ +package com.ankurm.jmm; + +/** + * Same shape as {@link Payload}, except {@code a} and {@code b} are {@code final}. + * + *

The JLS gives final fields a stronger guarantee than ordinary fields: if the object is + * constructed correctly — meaning no reference to {@code this} escapes during the constructor — + * then any thread that later observes a reference to it, by any means, is guaranteed to see the + * values the final fields were given in the constructor. No {@code volatile}, no lock, no other + * synchronization required. This is the mechanism the "initialization-on-demand holder" idiom + * and constant-holder classes lean on. + * + *

Explained in: the "what defaults do not do" section of the post. + */ +public final class FinalPayload { + public final int a; + public final int b; + + public FinalPayload(int a, int b) { + this.a = a; + this.b = b; + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/HolderIdiomSingleton.java b/jmm/src/main/java/com/ankurm/jmm/HolderIdiomSingleton.java new file mode 100644 index 0000000..42ecce0 --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/HolderIdiomSingleton.java @@ -0,0 +1,30 @@ +package com.ankurm.jmm; + +/** + * The initialization-on-demand holder idiom: no {@code volatile}, no explicit lock, and no + * double-checked anything. + * + *

It works because class initialization is itself synchronized by the JVM (JLS 12.4.2): the + * first thread to touch {@code Holder} runs its {@code }, every other thread that + * touches it blocks until {@code } completes, and the JLS guarantees a happens-before + * edge from the end of {@code } to every subsequent use of the class. {@code INSTANCE} + * does not even need to be {@code final} for this to hold — the guarantee comes from class + * initialization, not from the field modifier — though marking it final documents the intent + * and costs nothing. + * + *

This is the idiom to reach for in new code. The two classes above exist to show what it + * replaces and why. + */ +public final class HolderIdiomSingleton { + + private HolderIdiomSingleton() { + } + + private static final class Holder { + static final Payload INSTANCE = new Payload(1, 1); + } + + public static Payload getInstance() { + return Holder.INSTANCE; + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/Payload.java b/jmm/src/main/java/com/ankurm/jmm/Payload.java new file mode 100644 index 0000000..42ef7ab --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/Payload.java @@ -0,0 +1,27 @@ +package com.ankurm.jmm; + +/** + * The "expensive object" a singleton getter would normally construct and cache. + * + *

The two int fields are deliberately not {@code final}. That is the whole point of + * this class: a reader that observes a non-null reference to a {@code Payload} is only + * guaranteed to see fully-initialized fields if there is a happens-before edge between the + * constructor and the read. Without one, {@code a} and {@code b} can appear inconsistent — + * one updated, the other still its default {@code 0} — even though the constructor always + * sets both together. + * + *

See {@link FinalPayload} for the version that removes the bug a different way, by making + * the fields {@code final} instead of making the reference {@code volatile}. + * + *

Explained in: the "smallest correct mental model" and "how it really works underneath" + * sections of the post. + */ +public final class Payload { + public int a; + public int b; + + public Payload(int a, int b) { + this.a = a; + this.b = b; + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/PlainPublicationTest.java b/jmm/src/main/java/com/ankurm/jmm/PlainPublicationTest.java new file mode 100644 index 0000000..68d1c66 --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/PlainPublicationTest.java @@ -0,0 +1,45 @@ +package com.ankurm.jmm; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.II_Result; + +import static org.openjdk.jcstress.annotations.Expect.*; + +/** + * Reproduces the exact hazard inside {@link BrokenDclSingleton#getInstance()}'s unsynchronized + * publish: one thread builds a {@link Payload} and stores it into a plain (non-volatile) + * reference field; a second thread reads that field with no synchronization at all. + * + *

Run: {@code java -jar target/jcstress.jar -t PlainPublicationTest -v}
+ * Captured run: {@code output/01-plain-publication.txt} + */ +@JCStressTest +@Outcome(id = "1, 1", expect = ACCEPTABLE, + desc = "Reader saw the fully-constructed Payload.") +@Outcome(id = "0, 0", expect = ACCEPTABLE, + desc = "Reader ran before the write was published at all.") +@Outcome(id = {"1, 0", "0, 1"}, expect = ACCEPTABLE_INTERESTING, + desc = "Reordering: the reference was visible before both of its fields were. " + + "This is the double-checked-locking bug.") +@State +public class PlainPublicationTest { + + Payload instance; + + @Actor + public void writer() { + instance = new Payload(1, 1); + } + + @Actor + public void reader(II_Result r) { + Payload p = instance; + if (p == null) { + r.r1 = 0; + r.r2 = 0; + } else { + r.r1 = p.a; + r.r2 = p.b; + } + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/VolatileDclSingleton.java b/jmm/src/main/java/com/ankurm/jmm/VolatileDclSingleton.java new file mode 100644 index 0000000..f4d1643 --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/VolatileDclSingleton.java @@ -0,0 +1,33 @@ +package com.ankurm.jmm; + +/** + * The JSR-133 fix for double-checked locking: mark {@code instance} {@code volatile}. + * + *

Since 2004, a write to a volatile field happens-before every subsequent read of that same + * field (JLS 17.4.5). That single edge is enough: the constructor's writes to {@code a} and + * {@code b} happen-before the volatile write of {@code instance}, which happens-before any read + * of {@code instance} that observes the new reference, which happens-before the reads of + * {@code a} and {@code b} that follow it. The whole chain is transitive, so a reader that sees + * the reference at all is guaranteed to see a fully-built {@link Payload}. + * + *

Verified in {@code VolatilePublicationTest} — see {@code output/02-volatile-publication.txt} + * for the run that shows zero occurrences of the torn outcome across the same iteration count + * that produced them in the unsynchronized version. + */ +public final class VolatileDclSingleton { + private static volatile Payload instance; + + private VolatileDclSingleton() { + } + + public static Payload getInstance() { + if (instance == null) { + synchronized (VolatileDclSingleton.class) { + if (instance == null) { + instance = new Payload(1, 1); + } + } + } + return instance; + } +} diff --git a/jmm/src/main/java/com/ankurm/jmm/VolatilePublicationTest.java b/jmm/src/main/java/com/ankurm/jmm/VolatilePublicationTest.java new file mode 100644 index 0000000..4a8790c --- /dev/null +++ b/jmm/src/main/java/com/ankurm/jmm/VolatilePublicationTest.java @@ -0,0 +1,48 @@ +package com.ankurm.jmm; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.II_Result; + +import static org.openjdk.jcstress.annotations.Expect.*; + +/** + * Same race as {@link PlainPublicationTest}, except {@code instance} is {@code volatile} — + * exactly what {@link VolatileDclSingleton} does differently from {@link BrokenDclSingleton}. + * + *

The torn outcomes are marked {@code FORBIDDEN} here on purpose: if jcstress ever observed + * one, that would mean the write-happens-before-read guarantee for volatile fields (JLS 17.4.5) + * does not hold on this JVM/hardware combination, which would be a much bigger story than a + * blog post. It has not happened in any run behind this article. + * + *

Run: {@code java -jar target/jcstress.jar -t VolatilePublicationTest -v}
+ * Captured run: {@code output/02-volatile-publication.txt} + */ +@JCStressTest +@Outcome(id = "1, 1", expect = ACCEPTABLE, + desc = "Reader saw the fully-constructed Payload.") +@Outcome(id = "0, 0", expect = ACCEPTABLE, + desc = "Reader ran before the write was published at all.") +@Outcome(id = {"1, 0", "0, 1"}, expect = FORBIDDEN, + desc = "Would mean the volatile happens-before edge failed to hold.") +@State +public class VolatilePublicationTest { + + volatile Payload instance; + + @Actor + public void writer() { + instance = new Payload(1, 1); + } + + @Actor + public void reader(II_Result r) { + Payload p = instance; + if (p == null) { + r.r1 = 0; + r.r2 = 0; + } else { + r.r1 = p.a; + r.r2 = p.b; + } + } +} diff --git a/jmm/src/test/java/com/ankurm/jmm/SingletonBehaviorTest.java b/jmm/src/test/java/com/ankurm/jmm/SingletonBehaviorTest.java new file mode 100644 index 0000000..c34c470 --- /dev/null +++ b/jmm/src/test/java/com/ankurm/jmm/SingletonBehaviorTest.java @@ -0,0 +1,46 @@ +package com.ankurm.jmm; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Ordinary functional sanity checks for the three singleton implementations. These do NOT prove + * the memory-visibility behaviour claimed in the post — a single JUnit run cannot reliably + * observe a race that needs billions of interleavings to show up. That evidence comes from the + * jcstress tests in this package instead; this class only confirms the three singletons behave + * identically from a single thread's point of view, which is the property a reader would check + * first when adapting one of these classes. + */ +class SingletonBehaviorTest { + + @Test + void brokenSingletonReturnsConsistentInstance() { + Payload first = BrokenDclSingleton.getInstance(); + Payload second = BrokenDclSingleton.getInstance(); + assertSame(first, second); + assertEquals(1, first.a); + assertEquals(1, first.b); + } + + @Test + void volatileSingletonReturnsConsistentInstance() { + Payload first = VolatileDclSingleton.getInstance(); + Payload second = VolatileDclSingleton.getInstance(); + assertSame(first, second); + assertEquals(1, first.a); + assertEquals(1, first.b); + } + + @Test + void holderIdiomReturnsConsistentInstance() { + Payload first = HolderIdiomSingleton.getInstance(); + Payload second = HolderIdiomSingleton.getInstance(); + assertSame(first, second); + assertNotNull(first); + assertEquals(1, first.a); + assertEquals(1, first.b); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f76f0bf --- /dev/null +++ b/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.ankurm + java-core-examples + 1.0 + pom + + java-core-examples + Runnable companion code for ankurm.com's Java core / concurrency series. One module per article. + + + jmm + + + + 25 + 25 + UTF-8 + +