Java Memory Model Explained: How the volatile Keyword Controls Visibility and Ordering
The Java Memory Model (JMM) defines the rules for how threads interact through memory, and the volatile keyword ensures cross-thread visibility and prevents instruction reordering by inserting memory barriers that flush writes to main memory.
The Java Memory Model is the formal specification that governs thread communication and memory consistency in Java applications. According to the CyC2018/CS-Notes repository, which documents these concurrency internals in notes/Java 并发.md, the JMM abstracts away hardware and operating system details to guarantee consistent program behavior across all platforms.
What Is the Java Memory Model?
The Java Memory Model specifies how and when changes made by one thread become visible to other threads. It creates a layer of abstraction between the Java program and the underlying CPU architecture, hiding differences between processor caches and memory hierarchies.
Main Memory vs. Working Memory
In the JMM architecture, all variables reside in main memory (shared by all threads). However, each thread maintains its own working memory (local CPU caches or registers) that holds copies of the variables it reads. Threads cannot directly read or write main memory; instead, they operate on their working memory copies, and all value exchanges must synchronize through main memory.
As detailed in notes/Java 并发.md, this two-tiered structure is fundamental to understanding race conditions and visibility issues in concurrent programs.
Memory-Interaction Operations
The JMM defines eight low-level primitive actions that move data between main memory and working memory:
lock– Acquire a monitor lockunlock– Release a monitor lockread– Transfer a value from main memory to a thread's working memory (forload)load– Place thereadvalue into a working memory variableuse– Transfer a value from working memory to the JVM execution engineassign– Receive a value from the execution engine into working memorystore– Transfer a value from working memory to main memory (forwrite)write– Place thestoredvalue into a main memory variable
These eight actions form the foundation of all thread-memory interactions in Java.
The Three Core Guarantees
The JMM provides three fundamental guarantees that govern concurrent execution:
-
Atomicity: Each of the eight memory operations is indivisible. However, for non-volatile 64-bit types (
longanddouble), the JVM may split a single write or read into two 32-bit operations, breaking atomicity unless the variable is declaredvolatile. -
Visibility: A write to a variable is ultimately flushed to main memory, and a read obtains the latest value from main memory. Without proper synchronization, threads may see stale cached values.
-
Ordering: The JVM and CPU may reorder instructions for performance unless a happens-before relationship (established via
volatile,synchronized, or other synchronization mechanisms) forces a specific execution order.
How the volatile Keyword Affects the JMM
The volatile keyword is a field modifier that directly influences the visibility and ordering guarantees of the Java Memory Model. When applied to a variable, it changes how the JVM handles memory barriers and cache coherence.
Visibility Guarantees
Every write to a volatile variable is immediately flushed to main memory, bypassing the normal delay that might keep values in a thread's working memory. Correspondingly, every read of a volatile variable loads the latest value directly from main memory rather than a potentially stale local cache.
As implemented in the JVM according to notes/Java 并发.md, this ensures that once a thread writes to a volatile field, all other threads will see that update immediately.
Memory Barriers and Instruction Ordering
To enforce ordering, the JVM inserts specific memory barriers around volatile operations:
- A store barrier is inserted after a write to a
volatilefield. This barrier blocks the compiler and CPU from moving subsequent write operations before the volatile write. - A load barrier is inserted before a read of a
volatilefield. This prevents earlier read operations from being reordered after the volatile read.
These barriers prevent instruction reordering across the volatile access point, ensuring that memory actions before the write (or after the read) cannot migrate past the volatile boundary.
The Happens-Before Relationship
The JMM defines a specific happens-before rule for volatile variables: a write to a volatile field happens-before every subsequent read of that same field. This creates a partial ordering of operations that developers can rely on for safe publication of immutable objects or lightweight synchronization between threads.
This relationship is the formal mechanism that allows volatile to work correctly for flags and status indicators without the full overhead of synchronized blocks.
Limitations of volatile
volatile does not provide atomicity for compound actions. While it guarantees that individual reads and writes are visible and ordered, it does not make operations like count++ atomic. A count++ operation compiles to a read-modify-write sequence, and without additional synchronization, race conditions can still occur between threads.
Practical Code Examples
The following examples demonstrate correct and incorrect uses of volatile based on the JMM specification.
Safe Publication with volatile
Use volatile to ensure other threads see a fully initialized object:
class Holder {
// volatile guarantees other threads see a fully-initialized object
private volatile Data data;
void publish(Data d) {
data = d; // (1) write volatile – store barrier flushes to main memory
}
Data consume() {
return data; // (2) read volatile – load barrier fetches from main memory
}
}
Incorrect Use for Atomic Increment
Do not use volatile when you need atomic compound operations:
class Counter {
private volatile int cnt = 0; // visibility only, not atomicity
void inc() {
cnt++; // compiled to read-modify-write; race condition remains possible
}
}
Correct Thread-Safe Counter
For atomic compound operations, use java.util.concurrent.atomic classes:
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private final AtomicInteger cnt = new AtomicInteger();
void inc() {
cnt.incrementAndGet(); // atomic compare-and-swap, respects JMM visibility
}
}
Summary
- The Java Memory Model defines how threads interact through main memory and working memory, abstracting hardware-specific cache implementations.
- The JMM specifies eight primitive memory operations (
read,load,use,assign,store,write,lock,unlock) and provides guarantees for atomicity, visibility, and ordering. - The
volatilekeyword ensures visibility by flushing writes immediately to main memory and loading fresh values on read. volatileinserts memory barriers (store barriers after writes, load barriers before reads) that prevent instruction reordering and establish happens-before relationships.volatiledoes not provide atomicity for compound actions like increment operations; useAtomicIntegerorsynchronizedfor such cases.
Frequently Asked Questions
Does volatile guarantee atomicity for all operations?
No. volatile only guarantees atomicity for single read or write operations on the variable itself. Compound actions such as count++ (which involves reading, incrementing, and writing) are not atomic even with volatile. For atomic compound operations, use java.util.concurrent.atomic classes or synchronized blocks.
What is the difference between volatile and synchronized?
synchronized provides both mutual exclusion (only one thread executes the block at a time) and visibility (flushes memory), whereas volatile provides only visibility and ordering guarantees without mutual exclusion. volatile is lighter-weight but cannot protect compound operations or critical sections requiring exclusive access.
When should I use volatile instead of AtomicInteger?
Use volatile for simple flags, state indicators, or safe publication of immutable objects where you only need to ensure that the latest value is visible to all threads. Use AtomicInteger when you need to perform compound operations like incrementAndGet(), compareAndSet(), or any read-modify-write sequence that must execute atomically.
How do memory barriers work in the JVM?
Memory barriers are low-level CPU instructions (or compiler constraints) that prevent reordering of load and store operations. A store barrier (SFENCE) ensures all writes before the barrier complete before any writes after it, while a load barrier (LFENCE) ensures reads complete in order. The JVM inserts these automatically around volatile accesses to enforce the JMM ordering guarantees without requiring explicit programmer intervention.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →