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
@@ -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);
}
}