Java Memory Model (JMM): Understanding Memory Visibility and Ordering
When two threads access the same variable, the results can be surprising. A write performed by one thread may not be visible to another immediately—or ever—unless the program establishes a formal relationship between the two actions. The Java Memory Model (JMM) is the specification that defines exactly when such visibility is guaranteed.
The JMM is not a physical description of caches or memory buses. It is a language‑level contract that tells you, the Java engineer, what the JVM must ensure, regardless of the underlying hardware. It gives compilers and CPUs freedom to reorder and optimize, while still allowing you to reason about concurrency with mathematical precision.
This article builds a rigorous mental model of the JMM. You will learn why unsynchronized shared state fails, what volatile and synchronized actually guarantee, how the happens‑before relation works, and how to publish objects safely across threads. This is foundational knowledge for every topic that follows: locks, atomics, concurrent collections, virtual threads, and performance engineering.
1. The Core Problem​
Consider two threads and a shared variable:
class SharedState {
private int value = 0;
void set(int v) {
value = v;
}
int get() {
return value;
}
}
If Thread A calls set(42) and Thread B calls get(), what does Thread B see? Without synchronization, the answer is not defined. Thread B might see 42, or it might see the initial value 0, or—in the absence of a proper happens‑before relationship—the behavior is undefined.
This is not a bug in your code. It is the natural consequence of allowing compilers, JIT optimizers, and CPUs to reorder operations for performance. The JMM exists to give you a set of rules that, when followed, guarantee predictable visibility and ordering.
2. What the JMM Defines​
The JMM is a formal model that specifies:
- When a write to a shared variable becomes visible to another thread.
- Which operations are atomic (appear indivisible).
- What orderings of operations are legal, and which orderings are guaranteed.
- What constitutes a data race, and that programs with data races have undefined semantics.
The JMM allows a JVM implementation to be highly optimized, but it imposes constraints that ensure correctly synchronized programs behave consistently across all platforms.
3. Core Concepts​
| Concept | Meaning | Typical Failure Without It |
|---|---|---|
| Visibility | A thread sees the latest value written by another thread. | Stale value – reading an old value. |
| Atomicity | An operation is indivisible; no intermediate state is observable. | Lost update – one write overwrites another. |
| Ordering | The sequence of operations as observed across threads. | Unexpected result due to reordering. |
| Happens‑before | The formal relation that defines guaranteed visibility and ordering. | Data race – undefined behavior. |
Visibility​
Visibility means that when one thread writes a value, another thread can see that value. Without synchronization, the JMM does not guarantee visibility—a thread may continue reading a stale copy indefinitely.
Atomicity​
An atomic operation either happens completely or not at all. Simple reads and writes of int, boolean, and references are atomic. However, compound operations like value++ are not atomic: they consist of a read, a modify, and a write. Two threads can interleave these steps, producing a lost update.
Ordering​
In a single thread, operations appear to execute in program order. Across threads, there is no such guarantee. The JMM permits reordering as long as the single‑thread semantics of each thread are preserved. This reordering can cause cross‑thread surprises unless you establish ordering constraints with synchronization.
Happens‑Before​
The happens‑before relation is the backbone of the JMM. If action A happens‑before action B, then A’s effects are visible to B. Happens‑before is transitive. It is the formal rule that connects visibility and ordering.
4. Visibility in Practice​
Take a simple stop‑flag pattern:
public class Task {
private boolean running = true; // not volatile
public void stop() {
running = false;
}
public void run() {
while (running) {
// do work
}
}
}
One thread calls stop(), another is busy in run(). Without synchronization, the run() loop may never see the update. The compiler or CPU may cache the value of running, or optimize the loop because it sees no writes within the loop. The program can hang forever.
The fix is to make the flag volatile:
private volatile boolean running = true;
volatile ensures that a write to running is visible to every subsequent read of running by any thread. It also prevents reordering that would compromise that visibility.
5. Atomicity vs. Visibility​
volatile solves visibility, not atomicity. Consider a counter:
private volatile int counter = 0;
public void increment() {
counter++; // still not atomic!
}
counter++ is three operations: read, add one, write. Even with volatile, two threads can interleave:
Thread 1: read counter (0)
Thread 2: read counter (0)
Thread 1: write 1
Thread 2: write 1 // lost update: should be 2
The final value is 1, not 2. To make the increment atomic, use AtomicInteger:
private final AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet(); // atomic
}
This is a crucial distinction: visibility ensures you see the latest value; atomicity ensures your read‑modify‑write sequence is indivisible.
6. Instruction Reordering​
Compilers and CPUs reorder instructions to improve performance. For single‑threaded code, this is invisible. For multi‑threaded code, it can be catastrophic.
Consider two shared variables:
int a = 0;
boolean ready = false;
// Thread 1
a = 42;
ready = true;
// Thread 2
if (ready) {
System.out.println(a); // may print 0!
}
Intuitively, if ready is true, then a should be 42. But without synchronization, the compiler can reorder Thread 1’s writes: ready = true may be written before a = 42. CPU caches can similarly reorder. Thread 2 sees ready == true and reads a while it is still 0.
Reordering is constrained by the JMM’s happens‑before rules. To guarantee the intended order, you must establish a synchronization relationship between the write to ready and the read of ready.
7. Happens‑Before: The Formal Foundation​
The happens‑before relation is a partial order on actions. It is defined by the Java Language Specification and gives you a precise way to reason about concurrency.
The major rules are:
Program Order Rule​
Within a single thread, every action happens‑before the next action in program order.
Monitor Lock Rule​
An unlock on a monitor happens‑before a subsequent lock on the same monitor.
Volatile Rule​
A write to a volatile field happens‑before a subsequent read of that same field.
Thread Start Rule​
Calling Thread.start() happens‑before any action in the started thread.
Thread Termination Rule​
All actions in a thread happen‑before another thread successfully returns from Thread.join().
Transitivity​
If A happens‑before B, and B happens‑before C, then A happens‑before C.
These rules compose. For example, a synchronized block acts as both a lock and an unlock; the monitor lock rule thus creates a chain from the releasing thread to the acquiring thread. A volatile write followed by a volatile read creates a chain across threads.
Diagrammatically:
Thread 1: Thread 2:
│ │
│ (actions) │
│ │
│ unlock (monitor)│
â–Ľ â–Ľ
happens‑before
â–Ľ â–Ľ
│ lock (monitor) │
│ │
│ (actions) │
â–Ľ â–Ľ
Happens‑before is the tool you use to prove that one thread’s write will be visible to another thread’s read.
8. The volatile Keyword​
volatile is a field modifier that establishes specific memory semantics:
- Visibility guarantee – a write to a
volatilefield is always visible to a subsequent read of that field. - Ordering guarantee – the JMM prohibits certain reorderings around
volatileaccesses. Writes before thevolatilewrite stay before; reads after thevolatileread stay after.
class Configuration {
private volatile boolean initialized = false;
private Map<String, String> settings = new HashMap<>();
void init() {
settings.put("timeout", "30s");
settings.put("debug", "false");
initialized = true; // volatile write publishes everything before it
}
Map<String, String> getSettings() {
if (initialized) { // volatile read
return settings;
}
return null;
}
}
Here, the volatile write to initialized happens‑before any thread reads initialized. That means all writes before the volatile write—including the writes to settings—are visible to any thread that sees initialized == true.
This is a common safe‑publication pattern: use a volatile reference or flag to publish fully constructed state.
However, volatile does not make compound operations atomic. Use it for state flags, configuration publishing, and one‑writer/multiple‑reader scenarios where the writer only sets a single value.
9. synchronized and the JMM​
synchronized provides two guarantees:
- Mutual exclusion – only one thread at a time can execute the protected block.
- Memory visibility – all writes made before leaving a
synchronizedblock are visible to any thread that subsequently enters a block synchronized on the same monitor.
class Counter {
private int value = 0;
synchronized void increment() {
value++; // read‑modify‑write now safe
}
synchronized int get() {
return value; // visible
}
}
The monitor lock rule states that an unlock happens‑before a subsequent lock. Therefore, the write to value in increment() happens‑before the read in get() when they are synchronized on the same object.
This is more powerful than volatile for compound actions because the lock protects the entire read‑modify‑write sequence.
10. final Fields and Safe Construction​
The JMM provides special guarantees for final fields: once an object is properly constructed, the values of its final fields are visible to all threads without any additional synchronization—provided the object reference is not published before construction completes.
public class ImmutableConfig {
private final String host;
private final int port;
public ImmutableConfig(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() { return host; }
public int getPort() { return port; }
}
If you safely publish an ImmutableConfig instance (via a volatile reference, synchronized block, or static initializer), any thread that sees the object reference will see the correctly initialized final fields.
The key caveat: do not let the this reference escape during construction (e.g., by registering the object in a listener inside the constructor). That would break the guarantee.
11. Safe Publication​
Safe publication means making an object visible to other threads in a way that guarantees they see a fully constructed object, including all non‑final fields written during construction.
Unsafe publication:
public Holder holder;
public void initialize() {
holder = new Holder(42); // unsafe: no synchronization
}
Another thread may see the holder reference before construction finishes, or see a partially constructed Holder with default field values.
Safe publication techniques:
- Static initialization – objects created during class loading are published safely by the JVM.
volatilereference – write the object to avolatilefield after construction completes.synchronizedblock – write and read the reference inside synchronized blocks on the same monitor.AtomicReference– useset()andget()on an atomic reference.- Concurrent collections – e.g., putting an object into a
ConcurrentHashMapsafely publishes it to any reader.
private final AtomicReference<Holder> holderRef = new AtomicReference<>();
public void initialize() {
holderRef.set(new Holder(42)); // safe publication
}
12. Data Races​
A data race occurs when two or more threads access the same shared variable, at least one access is a write, and there is no happens‑before relationship between the accesses.
Programs with data races have undefined semantics under the JMM. The JVM is free to produce almost any result—not just stale values, but values that never existed in any sequential execution.
Example:
public class RaceExample {
private int x = 0; // shared, unsynchronized
public void writer() {
x = 42; // write
}
public void reader() {
System.out.println(x); // read – data race with writer
}
}
Two threads invoking writer() and reader() concurrently have a data race. The reader() might print 0, 42, or in pathological cases, some other value.
Data races are notoriously difficult to reproduce. Tests may pass thousands of times before a race manifests. The only reliable way to avoid them is to establish happens‑before relationships with volatile, synchronized, locks, atomics, or concurrent collections.
13. JMM and Hardware​
It is tempting to equate the JMM with CPU cache coherence. They are related but distinct.
- CPU caches are a hardware mechanism. Processors have local caches, store buffers, and reorder buffers that affect memory visibility.
- Memory barriers/fences are CPU instructions that enforce ordering and visibility at the hardware level.
- Cache coherence protocols (e.g., MESI) ensure that multiple CPU cores maintain a consistent view of memory, but they do not alone solve the JMM’s ordering problem.
The JMM is a language‑level abstraction. It defines what a JVM must guarantee, not how. A JVM on x86 may implement volatile with a simple store/load barrier; on ARM, stronger fences may be needed. But as a Java engineer, you reason in terms of JMM guarantees, not specific CPU instructions.
Understanding the hardware helps you appreciate why the JMM exists, but it is not a substitute for knowing the language rules.
14. Common Concurrency Patterns​
Stop Flag​
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// work
}
}
volatile is sufficient here because the only operation is a single write and a single read.
Lazy Initialization​
Simple thread‑safe lazy initialization:
private volatile ExpensiveObject instance;
public ExpensiveObject getInstance() {
ExpensiveObject local = instance;
if (local == null) {
synchronized (this) {
local = instance;
if (local == null) {
instance = local = new ExpensiveObject();
}
}
}
return local;
}
This is the correctly implemented double‑checked locking idiom. The volatile field is essential; without it, another thread could see a partially constructed object.
Immutable Objects​
Immutable objects are inherently thread‑safe. If all fields are final and the object is safely published, no further synchronization is needed for reads. This is why functional programming and immutable state are so valuable in concurrent systems.
Concurrent Collections​
ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue provide internal synchronization, establishing happens‑before relationships between producers and consumers. Using them correctly eliminates many explicit synchronization needs.
15. Common Mistakes​
- Assuming
volatilemakes compound operations atomic. It does not.volatile counter++is still a lost‑update bug. - Assuming
++is atomic. It is a read‑modify‑write. UseAtomicIntegerorsynchronized. - Assuming source order equals cross‑thread order. Reordering can violate that assumption.
- Using unsynchronized shared mutable state. Every shared field accessed by multiple threads needs a synchronization policy.
- Publishing objects before construction completes. Do not let
thisescape from the constructor. - Believing a passing test proves no data race. Races are probabilistic. A passing test provides no guarantee.
- Relying on a specific CPU’s behavior. The JMM is the contract; hardware behavior varies.
- Using
Thread.sleep()as synchronization. Sleep does not establish happens‑before relationships.
16. JMM and Java Concurrency Utilities​
The java.util.concurrent package builds on JMM guarantees:
- Atomic classes (
AtomicInteger,AtomicReference,LongAdder) provide atomic compound operations with volatile‑like visibility. ReentrantLockprovides the same memory semantics assynchronized: unlock happens‑before lock.ConcurrentHashMapsafely publishes entries: putting an object into the map happens‑before another thread retrieves it.CountDownLatch,Semaphore,CyclicBarriercreate happens‑before chains across threads.ExecutorServiceandCompletableFuturemanage task submission and completion with defined visibility.
When you use these utilities, the JMM’s guarantees are already built in. You get thread safety without manually reasoning about every field.
17. Practical Example: Thread‑Safe State Flag​
Broken version:
public class Worker {
private boolean stopped = false; // not volatile, not synchronized
public void requestStop() {
stopped = true;
}
public void run() {
while (!stopped) {
// do work
}
}
}
This may run forever because the run() thread does not see the write.
Fixed with volatile:
public class Worker {
private volatile boolean stopped = false;
public void requestStop() {
stopped = true; // visible to any thread that reads `stopped`
}
public void run() {
while (!stopped) {
// do work
}
}
}
volatile is sufficient because the flag is a single boolean. No compound operation is involved.
When volatile is not enough:
private volatile int count = 0;
public void increment() {
count++; // unsafe! use AtomicInteger or synchronized
}
Use AtomicInteger for the counter, and reserve volatile for simple state flags and safe publication.
18. JMM Troubleshooting Guide​
| Symptom | Possible Cause | JMM Concept |
|---|---|---|
| Stale value read by thread | Missing volatile or lock | Visibility |
| Counter undercounts (lost update) | Compound operation without atomicity | Atomicity |
| Unexpected value order | Reordering of writes/reads | Ordering / happens‑before |
| Intermittent bug, passes tests | Unsynchronized shared state | Data race |
| Thread sees partially built object | Unsafe publication | Safe publication |
| Class initialization failure | Static initializer threw exception | Initialization / happens‑before |
When diagnosing, ask three questions:
- Is the shared state properly synchronized?
- What is the happens‑before relationship between the writer and reader?
- Is the operation atomic, or is it a compound action that needs a lock or atomic class?
19. JMM Concepts for Interviews​
Senior Java interviews frequently probe the JMM:
- Visibility vs. atomicity – can you articulate the difference and give examples?
volatilesemantics – what it guarantees and what it does not.synchronizedmemory guarantees – mutual exclusion plus visibility.- Happens‑before rules – name at least three and explain their significance.
- Instruction reordering – why it happens and how to prevent it when needed.
- Data races – definition and consequences under the JMM.
- Safe publication – how to publish objects correctly; double‑checked locking.
finalfield semantics – how immutability enables safe publication.
Do not memorize answers. Build the mental model, and the answers follow naturally.
20. JMM Learning Roadmap​
Java Threads
↓
Shared State
↓
Visibility
↓
Atomicity
↓
Ordering
↓
Happens‑Before
↓
volatile / synchronized
↓
Safe Publication
↓
Thread‑Safe Design
Begin with threads and shared state to understand the problem. Then learn visibility, atomicity, and ordering as separate concepts. Happens‑before ties them together. Only then study volatile and synchronized as tools that establish those relationships. Safe publication and thread‑safe design patterns are the application of everything before them.
21. Frequently Asked Questions​
What is the Java Memory Model? The JMM is a formal specification that defines how threads interact with shared memory. It specifies when writes become visible, what operations are atomic, what orderings are guaranteed, and what constitutes a data race.
What problem does the JMM solve? It provides a language‑level contract that allows compiler and CPU optimizations while still making correctly synchronized programs behave predictably across all platforms.
What is the difference between visibility and atomicity?
Visibility means a thread sees another thread’s write. Atomicity means an operation is indivisible. volatile provides visibility but not compound‑action atomicity. synchronized and atomics provide both.
Does volatile make ++ thread‑safe?
No. ++ is a compound operation (read, modify, write). volatile only guarantees visibility and ordering, not atomicity. Use AtomicInteger or a lock.
What is happens‑before? It is a formal relation between actions. If A happens‑before B, then A’s effects are visible to B. Major rules cover program order, monitor locks, volatile accesses, thread start/termination, and transitivity.
Does synchronized guarantee visibility?
Yes. An unlock happens‑before a subsequent lock on the same monitor, so all writes made inside the block are visible to the next thread that acquires the lock.
Why does instruction reordering matter? Reordering can cause cross‑thread surprises: one thread may observe writes in a different order than they appear in source code. The JMM defines what orderings are guaranteed and where synchronization is required.
What is safe publication?
Safe publication means making an object visible to other threads in a way that guarantees they see a fully constructed object. Techniques include volatile references, synchronized blocks, static initializers, and concurrent collections.
What is a data race? A data race occurs when two threads access the same variable, one is a write, and there is no happens‑before relationship. Programs with data races have undefined semantics.
Does Java guarantee sequential consistency for all programs? No. Sequential consistency is guaranteed only for correctly synchronized programs. For programs with data races, the JMM allows surprising, non‑sequential behavior.
22. Next Steps​
The JMM is the theoretical foundation. Continue to the practical side:
- Java Concurrency Model: Threads, Locks, and Synchronization – apply JMM rules in real concurrency constructs.
- Java Garbage Collection: How JVM Manages Memory – understand object lifecycles and memory reclamation.
- JIT Compiler in Java: From Bytecode to Machine Code – how runtime optimization interacts with concurrency.
- Virtual Threads in Java: Modern Concurrency with Project Loom – lightweight concurrency on top of the JMM.
For practical performance work, the Performance Engineering section shows how concurrency and memory behavior affect production systems.
23. Key Takeaways​
- The Java Memory Model defines the rules for how threads access shared memory.
- Four core concepts—visibility, atomicity, ordering, happens‑before—form the basis of all concurrency reasoning.
volatileprovides visibility and ordering, but not compound action atomicity.synchronizedprovides mutual exclusion and memory visibility through the monitor lock rule.- Safe publication is essential when sharing objects across threads; final fields and immutability simplify the problem.
- A data race is a program bug with undefined semantics; tests cannot prove its absence.
- Reason about concurrency using JMM guarantees, not hardware‑specific assumptions.
- The JMM underpins every concurrency utility in
java.util.concurrentand every concurrent design pattern you will encounter.
Mastering the JMM is a rite of passage for serious Java engineers. It is the difference between code that happens to work and code that is correct by construction.