Java Memory Model Tutorial: Happens-Before, volatile and Safe Publication
Learn the Java Memory Model through exact start/join, volatile, synchronized, final-field and AtomicInteger examples, with clear happens-before guarantees.
KnowledgeGate Team
Exam prep & CS education

One thread executes score = 42; ready = true;, yet another cannot treat those plain writes as one single-threaded trace. The Java Memory Model (JMM) defines legal observations, not stacks, heaps, CPU caches or garbage collection. Exact examples clarify visibility, ordering, atomicity and happens-before.
What the Java Memory Model controls
The JMM determines which shared-state write a read may observe. The rules come from Chapter 17 of the Java SE 21 Language Specification.
Under JLS 17.4.1, static int total, account.balance and scores[2] are shared variables when several threads can reach them. A local int subtotal = 6, a parameter or an exception parameter is not shared, although an object reached through a local reference may contain shared fields.
Ask three questions: visibility, can R observe W's value; ordering, must one action precede another; and atomicity, can another thread interleave with multiple steps?
Use Java Multithreading Basics: Threads, Runnable and Safe Counters with Examples for Thread/Runnable creation, lifecycle, scheduling and counter basics. The JMM explains why start(), join() and synchronization change what another thread may observe.
Build happens-before from four concrete edges
Program order covers one thread. Synchronization can create a synchronizes-with edge between threads. Happens-before contains both and their transitive consequences. If A happens-before B, A's effects are visible to and ordered before B. Wall-clock timing proves nothing.
JLS-backed rule | Exact application |
|---|---|
Monitor unlock to a later lock of the same monitor | Unlock |
Volatile write to a later read of the same field | Writing |
Thread start |
|
Thread termination and join | Actions in |
These rules come from JLS 17.4.4 and 17.4.5. Main writes input = 7 and starts worker. Worker reads 7, calculates 7 x 6 = 42, writes result = 42 and terminates. After join() returns, main reads 42. Start carries input in; join carries result back.
A data race is two conflicting accesses to one variable, at least one a write, without happens-before ordering. A correctly synchronized, data-race-free program appears sequentially consistent.

Runnable example: start and join publish 42
public class StartJoinVisibility {
static int input = 0;
static int result = 0;
public static void main(String[] args) throws InterruptedException {
input = 7;
Thread worker = new Thread(() -> result = input * 6, "calculator");
worker.start();
worker.join();
System.out.println("result=" + result);
}
}The output is result=42 because 7 x 6 = 42. Start publishes earlier actions to worker, and join publishes completed worker actions back. Neither field needs volatile here.
Remove join(), and main may print result=0 or result=42; no worker-to-main edge orders that read. Thread.sleep(100) is not a repair because JLS 17.3 gives sleep no synchronization semantics.
Runnable example: volatile hands off score 42
With plain score and ready, both fields race. The reader need not leave its loop, and source order does not publish across threads. Repair only ready:
public class VolatileHandoff {
static int score = 0;
static volatile boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!ready) Thread.onSpinWait();
System.out.println("score=" + score);
});
Thread writer = new Thread(() -> { score = 42; ready = true; });
reader.start(); writer.start();
reader.join(); writer.join();
}
}After reader observes volatile true, it must print score=42. Writer order, the volatile edge and reader order form one transitive path. This does not promise scheduling fairness. For production one-shot coordination, prefer CountDownLatch to busy spinning.

synchronized protects a 1,000 balance invariant
Visibility is insufficient when a check and update form one invariant:
import java.util.Arrays;
public class AccountDemo {
static class Account {
private int balance = 1_000;
synchronized boolean withdraw(int amount) {
if (balance < amount) return false;
balance -= amount;
return true;
}
synchronized int balance() { return balance; }
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
boolean[] success = new boolean[2];
Thread t1 = new Thread(() -> success[0] = account.withdraw(600));
Thread t2 = new Thread(() -> success[1] = account.withdraw(600));
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println(Arrays.toString(success) + ", balance=" + account.balance());
}
}The results are [true, false] or [false, true], with balance 400: 1,000 - 600 = 400, so the second check fails. Without synchronization, both can read 1,000, pass, calculate and write 400, then report success after accepting 600 + 600 = 1,200 against 1,000.
synchronized supplies exclusion and visibility through unlock and later lock. Synchronization in Java: Monitors, Race Conditions and Runnable Examples goes deeper into monitor identity, synchronized methods versus blocks, wait/notify and deadlock prevention. The 1,000-balance trace isolates the JMM distinction between visibility and a protected compound invariant.
Safe publication with final fields
An immutable configuration fixes its state during construction:
static final class Config {
final int workers, batchSize, capacity;
Config(int workers, int batchSize) {
this.workers = workers;
this.batchSize = batchSize;
this.capacity = workers * batchSize;
}
}
static volatile Config config;
// Publisher: config = new Config(4, 250);
// Reader: Config local = config;For new Config(4, 250), capacity is 4 x 250 = 1,000. A non-null local can print workers=4, batchSize=250, capacity=1000. The volatile reference publishes the object; final fields express construction-time immutability. JLS 17.5 gives correctly constructed objects special final-field guarantees when this does not escape.
final gives neither deep immutability nor safe later mutation. Without reference publication, a reader may miss the installed configuration.
Choose volatile, synchronized, atomic or final
Requirement | Tool | Example |
|---|---|---|
Publish an independent state transition |
|
|
Protect a compound invariant |
| Check and subtract balance |
Perform one atomic numeric update |
| Increment a counter |
Fix state during correct construction |
|
|
The invariant chooses the tool. Volatile access synchronizes, but a decrement is still read, subtract and write. From volatile int permits = 4, A and B can both read 4, compute 3 and write 3. Two serial decrements should leave 2. With AtomicInteger(4), two completed decrementAndGet() calls leave 2.
Sleep does not publish state. containsKey then put remains compound on a concurrent map, so use its suitable atomic method. Volatile fields cannot preserve a multi-field invariant; use one lock or publish an immutable snapshot. Happens-before constrains observations, not folklore about private copies or values stored only in main memory.
Java Memory Model exercises and the short version
Work through each scenario and name the guarantee that forces the answer:
Main sets
limit = 5, starts a worker that sums1 + 2 + 3 + 4 + 5, then joins. Main must read15.Writer performs ordinary
data = 64, then volatileready = true. After reader seestrue, it must read64. Reversing the writes removes that guarantee for the later data write.Balance starts at
1,500; two synchronized withdrawals request900. Exactly one succeeds, and1,500 - 900 = 600remains.An
AtomicIntegerstarts at5. After3 x 1,000 = 3,000completed increments and joins, the value is3,005.
For practice, explain each of the four answers before running code, then remove one ordering edge and predict which guarantee disappears.
The core guarantees are six rules: plain shared access may race; happens-before carries visibility and ordering; start() publishes earlier actions to a worker; join() publishes completed worker actions back; volatile can publish a state transition but cannot make count++ atomic; synchronized protects multi-step invariants. Continue with the Complete Java Course for the wider language sequence, or browse Coding & Skill Development Courses for the broader programming route.
Keep learning
TreeMap and SortedMap in Java: Ordering, Range Views and Worked Examples
Learn how Java's TreeMap orders keys, answers floor and ceiling queries, exposes live range views, and handles comparator collisions through worked examples.
HashSet and TreeSet in Java: Runnable Examples and Key Differences
Learn how Java sets handle duplicates, ordering, navigation and custom objects through deterministic examples you can run and modify.
Java Garbage Collection Tutorial: Reachability, Leaks and Runnable Examples
Trace three connected objects, find exactly when each becomes eligible for collection, and repair a cache that keeps byte arrays reachable.
Java Exception Handling Tutorial with Runnable Examples
Learn how Java moves control through try, catch, and finally, then build checked custom exceptions and safer file handling with runnable code.





