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:
@@ -0,0 +1,6 @@
|
|||||||
|
target/
|
||||||
|
*.class
|
||||||
|
jmm/jcstress-results-*.bin.gz
|
||||||
|
jmm/dependency-reduced-pom.xml
|
||||||
|
jmm/post.html
|
||||||
|
.DS_Store
|
||||||
@@ -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).
|
||||||
+21
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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>
|
||||||
Executable
+42
@@ -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."
|
||||||
Executable
+15
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>java-core-examples</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<name>java-core-examples</name>
|
||||||
|
<description>Runnable companion code for ankurm.com's Java core / concurrency series. One module per article.</description>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>jmm</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
</project>
|
||||||
Reference in New Issue
Block a user