Understanding the Java Memory Model (JMM) and Its Concurrency Rules
This article explains the Java Memory Model, describing how main memory and thread‑local working memory interact, the eight primitive actions defined by the JVM, the special semantics of volatile, long and double variables, and how these concepts underpin atomicity, visibility and the happens‑before principle in concurrent Java programs.
The Java Memory Model (JMM) is defined by the JVM specification to hide hardware and operating‑system differences, ensuring consistent memory access across platforms and preventing the platform‑specific bugs that can appear in C/C++ programs.
Physical Hardware and Memory
On a single‑core CPU, coordination between the processor and slower components (I/O, network, memory) is straightforward, but multi‑core systems must also guarantee data consistency when multiple cores access the same variables. Cache coherence protocols such as MSI/MESI help keep CPU caches and main memory synchronized.
Modern CPUs also perform out‑of‑order execution, later re‑ordering results to appear as if the program executed sequentially.
Java Memory Model
Although Java code runs inside a virtual machine, the VM itself runs on physical hardware, so the JMM mirrors the underlying hardware model. The model defines how variables are accessed, focusing on instance fields, static fields, and array elements (not local variables or method parameters, which are thread‑private).
Key concepts:
Main Memory : The shared memory area (conceptually the heap) where all variables must reside.
Working Memory : A thread‑private copy of variables (analogous to the JVM stack or CPU cache). Threads read from main memory into working memory, modify there, and write back.
Note: Main memory and working memory are not the same as the JVM's heap, stack, or method area; the terminology is used only for conceptual clarity.
Interaction Between Working Memory and Main Memory
The JVM defines eight atomic actions that move a variable between main and working memory:
lock : Exclusively lock a variable in main memory.
unlock : Release the lock.
read : Transfer the value from main memory to the thread.
load : Place the transferred value into the thread’s working memory.
use : Use the value from working memory for execution.
assign : Store a computed result back into working memory.
store : Transfer the working‑memory value back to main memory.
write : Write the transferred value into main memory.
These actions must obey a set of rules (e.g., read must be paired with load, assign must be paired with store, a variable must be locked before being accessed, etc.) to guarantee atomicity and visibility.
Special Rules for volatile Variables
The volatile keyword provides a lightweight synchronization mechanism. The JMM imposes three additional constraints on volatile variables:
A thread may execute use only immediately after a load , and vice‑versa.
A thread may execute store only immediately after an assign , and vice‑versa.
For two volatile variables V and W, the order of their associated actions preserves program order (no reordering).
Consequences:
Volatile variables are always visible to all threads.
Writes to volatile variables cannot be reordered with other memory actions, preventing the classic “check‑then‑act” race.
Example demonstrating that volatile does not make compound operations atomic:
public class VolatileTest {
public static volatile int race = 0;
public static void increase() { race++; }
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) { Thread.yield(); }
System.out.println(race);
}
}Even with volatile , the final count is often less than 200 000 because race++ is not atomic.
Special Rules for long and double
For non‑volatile 64‑bit primitives, the JVM may split reads/writes into two 32‑bit operations, making them non‑atomic. This is the “non‑atomic treatment of double and long variables.”
Essence of the Concurrent Memory Model
The JMM focuses on three properties:
Atomicity : Guarantees that the six basic actions (read, load, use, assign, store, write) are atomic; lock/unlock are provided via synchronized .
Visibility : Ensured by volatile and synchronized , which prevent reordering and flush caches.
Ordering (Happens‑Before) : Defined by a set of rules (program order, monitor lock, volatile write/read, thread start/join, interrupt, finalization, transitivity) that establish a partial order of actions across threads.
Understanding these rules helps developers write correct concurrent code without being misled by apparent execution order.
Happens‑Before Principle
An action A happens‑before action B if the effects of A are guaranteed to be visible to B. Examples include program order within a thread, monitor lock release before acquisition, volatile write before subsequent volatile read, thread start before any actions in the new thread, and thread termination before a join returns.
By applying these principles, developers can reason about thread safety and decide when to use synchronized or volatile to achieve the desired memory visibility and ordering guarantees.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.