Java Memory Model
The Java Memory Model (JMM) defines the rules that govern how threads interact with memory — what values a thread is guaranteed to see when it reads a variable written by another thread. Without understanding these rules, concurrent code can behave in mysterious, non-deterministic ways even when it looks perfectly correct.
Why a Memory Model?
Modern hardware doesn’t just execute instructions top to bottom. CPUs reorder instructions, processors have multiple levels of cache, and compilers optimize code by moving reads and writes around. Each thread effectively has its own “view” of memory until it synchronizes with others.
The JMM is the contract between you (the programmer) and the JVM/hardware. It says: “If you follow these rules, the JVM guarantees these visibility and ordering outcomes.” If you break the rules, all bets are off.
The Main Problem: Visibility and Reordering
Without proper synchronization, two things can go wrong:
- Visibility failure — Thread B reads a stale value of a variable that Thread A already updated.
- Reordering — The CPU or JIT compiler reorders your instructions, so Thread B sees operations in a different order than Thread A performed them.
Here is the classic visibility bug:
public class VisibilityBug {
private static boolean running = true; // no volatile!
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (running) { // may loop forever!
count++;
}
System.out.println("Stopped. count=" + count);
});
worker.start();
Thread.sleep(100);
running = false; // main thread writes false
System.out.println("Flag set to false");
}
}
The worker thread may never see running = false because the JIT can cache running in a CPU register. The program may run forever. This is not a bug in Java — it is the JMM working as specified when you skip synchronization.
The happens-before Relationship
The core concept of the JMM is the happens-before (HB) relationship. If action A happens-before action B, then:
- The results of A are visible to B.
- A appears to execute before B in program order.
The JMM guarantees happens-before in these situations:
| Rule | Description |
|---|---|
| Program order | Each action in a thread happens-before the next action in that same thread. |
| Monitor unlock | An unlock on a monitor happens-before every subsequent lock of the same monitor. |
| volatile write | A write to a volatile variable happens-before every subsequent read of that same variable. |
| Thread start | Thread.start() happens-before any action in the started thread. |
| Thread join | All actions in a thread happen-before Thread.join() returns. |
| Object construction | The completion of an object’s constructor happens-before the finalize() method begins. |
| Transitivity | If A HB B and B HB C, then A HB C. |
Note: “Subsequent” in the volatile/monitor rules means subsequent in wall-clock time, not just source-code order. Any thread that reads the volatile variable after the write sees the write.
volatile — The Lightweight Fix
Adding volatile to the flag above creates a happens-before edge between the write and every future read:
public class FixedVisibility {
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (running) {
count++;
}
System.out.println("Stopped. count=" + count);
});
worker.start();
Thread.sleep(100);
running = false;
System.out.println("Flag set to false");
}
}
Output:
Flag set to false
Stopped. count=<some large number>
volatile gives you visibility and atomicity for single reads/writes, but it does not make compound operations like count++ atomic. For that you need synchronization or the java.util.concurrent.atomic classes.
Warning:
volatiledoes NOT replacesynchronizedfor compound check-then-act sequences.if (flag) { doSomething(); }is still a race condition even ifflagis volatile.
synchronized and the Monitor Lock
Every synchronized block creates happens-before edges automatically:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // read-modify-write is safe inside a lock
}
public synchronized int getCount() {
return count;
}
}
When Thread A releases the lock (exits synchronized), everything it wrote is flushed to main memory. When Thread B acquires the same lock, it sees those writes. This is the JMM’s monitor unlock → monitor lock rule in action.
See synchronization and synchronized block for deeper coverage of locking strategies.
Safe Publication
Safe publication means making an object visible to other threads in a way that guarantees they see the fully-constructed object — not a partially-initialized one.
Unsafe publication
// UNSAFE — another thread might see a partially constructed Helper
public class Holder {
public Helper helper; // plain field, no synchronization
public void initialize() {
helper = new Helper(42); // may be visible before constructor finishes!
}
}
The JVM can reorder the write to helper before the constructor has finished writing all fields.
Safe publication patterns
// Pattern 1: volatile field
public class Holder {
public volatile Helper helper;
}
// Pattern 2: final field (guaranteed visible after construction)
public class Holder {
public final Helper helper;
public Holder() { this.helper = new Helper(42); }
}
// Pattern 3: static initializer (class loading is thread-safe)
public class Holder {
private static final Helper INSTANCE = new Helper(42);
public static Helper get() { return INSTANCE; }
}
// Pattern 4: synchronized accessor
public class Holder {
private Helper helper;
public synchronized Helper getHelper() { return helper; }
public synchronized void setHelper(Helper h) { helper = h; }
}
Tip:
finalfields are the safest option when your object is immutable. The JMM gives special guarantees for finals — all threads see their correct values after the constructor returns, with no need for extra synchronization.
The Double-Checked Locking Anti-Pattern
Before Java 5 (JSR-133 revised the JMM), double-checked locking was broken. Today, it works correctly only with volatile:
public class Singleton {
// volatile is REQUIRED here — without it, JMM allows broken visibility
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // first check (no lock)
synchronized (Singleton.class) {
if (instance == null) { // second check (with lock)
instance = new Singleton();
}
}
}
return instance;
}
}
Without volatile, the JIT can reorder the constructor write and the write to instance, exposing a half-constructed object to a second thread. With volatile, the happens-before rule ensures correctness.
Under the Hood
The JMM and Hardware Caches
Each CPU core has its own L1/L2 cache. Without synchronization, a write by Core A sits in Core A’s cache, invisible to Core B. The JMM maps its happens-before rules to memory barriers (also called memory fences) at the hardware level:
- A store barrier (StoreStore/StoreLoad) flushes a core’s write buffer to the shared cache or main memory.
- A load barrier (LoadLoad/LoadStore) forces a core to invalidate its cached copy and re-read from shared memory.
volatile writes emit a StoreLoad barrier — the most expensive kind — which is why volatile has a real (though usually small) performance cost compared to a plain field.
How the JIT Respects the JMM
The JIT compiler is free to reorder instructions within a happens-before boundary, but must not reorder across one. Bytecode inspection with javap rarely reveals this directly — the reordering happens at the native-code level. When you run with -XX:+PrintAssembly (requires hsdis), you can see the MFENCE (x86) or DMB (ARM) instructions the JIT emits for barriers.
JSR-133 and Java 5
The JMM was substantially clarified and strengthened in Java 5 (JSR-133). Before that, volatile did not create strong enough happens-before guarantees to prevent all data races. Modern Java (5+, including the current LTS Java 21) uses the JSR-133 model throughout. You can rely on all rules described on this page for any Java 5+ code.
Common Mistakes
- Checking a flag in a tight loop without
volatile— the JIT hoists the read outside the loop, causing the thread to spin forever. - Assuming
++is atomic —count++compiles to three bytecode instructions. Even on a 64-bit JVM, it is not atomic without synchronization orAtomicInteger. - Forgetting
volatileon the instance field in double-checked locking — broken until Java 5 and still wrong today if you omit it. - Publishing an object by storing it in a plain field — a partially-initialized object may be seen by other threads (use
final,volatile, or a lock).
Tip: The
java.util.concurrentpackage (see concurrent collections and ReentrantLock) builds its safety guarantees on top of exactly these JMM rules, so you get happens-before for free when you use its APIs correctly.
Related Topics
- volatile Keyword — the simplest way to establish visibility guarantees between threads
- Synchronization — using monitors and
synchronizedto enforce atomicity and ordering - Deadlock — what happens when threads compete for locks incorrectly
- ReentrantLock & Monitors — explicit locking that builds on the same JMM rules
- Callable & Future — task-based concurrency with built-in safe publication
- JIT Compilation & Bytecode — how the JIT optimizes code while respecting the JMM