How Volatile Enforces Memory Visibility and Prevents Reordering in Java
This article explains the Java volatile keyword by linking its visibility guarantees and happens‑before ordering to the underlying memory‑barrier implementation, illustrating the concepts with code examples, barrier diagrams, and a discussion of possible optimizations.
1. Volatile and happens‑before
The happens‑before principle is used to determine whether data races exist and whether threads are safe; it guarantees visibility in multithreaded environments. A classic example demonstrates how reads and writes of a volatile variable establish happens‑before relationships.
public class VolatileTest {
int i = 0;
volatile boolean flag = false;
// Thread A
public void write() {
i = 2; // 1
flag = true; // 2
}
// Thread B
public void read() {
if (flag) { // 3
System.out.println("---i = " + i); // 4
}
}
}According to the happens‑before rules, the program yields the following ordering:
Program order: 1 → 2, 3 → 4.
Volatile rule: 2 → 3.
Transitivity: 1 → 4.
Because operation 1 happens‑before operation 4, the write to i is visible to the read in operation 4. Volatile also prevents reordering, so the write to i cannot be moved after the volatile write, ensuring immediate visibility to other threads after they read the volatile flag.
2. Volatile memory semantics and implementation
In the Java Memory Model (JMM), communication between threads uses shared memory. When a volatile variable is written, the JVM flushes the thread's local copy of shared variables to main memory. When a volatile variable is read, the thread invalidates its local copy and reads directly from main memory.
Consequently, volatile writes have a write‑memory‑semantic of flushing to main memory, and volatile reads have a read‑memory‑semantic of loading from main memory.
To enforce these semantics, the JVM inserts memory‑barrier instructions around each volatile operation:
StoreStore barrier before every volatile write.
StoreLoad barrier after every volatile write.
LoadLoad barrier after every volatile read.
LoadStore barrier after every volatile read.
These barriers guarantee that:
All ordinary writes before a volatile write are flushed to main memory (StoreStore).
No reordering occurs between a volatile write and subsequent reads or writes (StoreLoad).
Volatile reads are not reordered with following ordinary reads (LoadLoad).
Volatile reads are not reordered with following ordinary writes (LoadStore).
The following example visualizes the barrier insertion for VolatileTest:
The JVM adopts a conservative strategy, inserting all four barriers even when some may be unnecessary. Optimizations can remove redundant barriers without changing the volatile write‑read semantics. An unoptimized diagram is shown below:
Analyzing the diagram reveals that barriers 2, 3, and 6 can be omitted because later volatile operations already prevent the corresponding reorderings. The optimized diagram is:
Thus, the JVM’s conservative barrier insertion ensures correctness, while practical implementations may safely omit certain barriers when they do not affect the volatile memory semantics.
Reference
Fang Tengfei, Java Concurrency Programming in Practice
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
