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
+21
View File
@@ -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.
+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.
+21
View File
@@ -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.
+21
View File
@@ -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.
+21
View File
@@ -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.
@@ -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)
+6
View File
@@ -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
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm</groupId>
<artifactId>java-core-examples</artifactId>
<version>1.0</version>
</parent>
<artifactId>jmm</artifactId>
<name>jmm</name>
<description>The Java Memory Model: volatile, happens-before, safe publication, and the double-checked-locking bug, reproduced with jcstress.</description>
<properties>
<jcstress.version>0.16</jcstress.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>jcstress</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
<!--
jcstress-core ships its own annotation processor (JCStressTestProcessor) that
generates the *_jcstress.java harness classes from @JCStressTest/@State/@Actor.
On JDK 21 javac still finds it implicitly via the classpath (with a warning that
this is going away). On JDK 25 that implicit discovery is gone: no warning, no
error, the processor is just silently never invoked and target/generated-test-sources
stays empty. Verified with plain javac on both JDKs while building this repo -
see the versions callout in the post. annotationProcessorPaths is what makes it
work on both.
-->
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jcstress.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+42
View File
@@ -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 <annotationProcessorPaths> 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."
+15
View File
@@ -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 <TestClassName> [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 "$@"
@@ -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.
*
* <p>{@code instance} is a <em>plain</em> 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}.
*
* <p>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;
}
}
@@ -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.
*
* <p>Run: {@code java -jar target/jcstress.jar -t FinalFieldPublicationTest -v}<br>
* 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;
}
}
}
@@ -0,0 +1,23 @@
package com.ankurm.jmm;
/**
* Same shape as {@link Payload}, except {@code a} and {@code b} are {@code final}.
*
* <p>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.
*
* <p>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;
}
}
@@ -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.
*
* <p>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 <clinit>}, every other thread that
* touches it blocks until {@code <clinit>} completes, and the JLS guarantees a happens-before
* edge from the end of {@code <clinit>} 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.
*
* <p>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;
}
}
@@ -0,0 +1,27 @@
package com.ankurm.jmm;
/**
* The "expensive object" a singleton getter would normally construct and cache.
*
* <p>The two int fields are deliberately <b>not</b> {@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.
*
* <p>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}.
*
* <p>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;
}
}
@@ -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.
*
* <p>Run: {@code java -jar target/jcstress.jar -t PlainPublicationTest -v}<br>
* 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;
}
}
}
@@ -0,0 +1,33 @@
package com.ankurm.jmm;
/**
* The JSR-133 fix for double-checked locking: mark {@code instance} {@code volatile}.
*
* <p>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}.
*
* <p>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;
}
}
@@ -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}.
*
* <p>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.
*
* <p>Run: {@code java -jar target/jcstress.jar -t VolatilePublicationTest -v}<br>
* 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;
}
}
}
@@ -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);
}
}