Understanding Java volatile: Visibility, Atomicity, and Instruction Reordering
This article explains the purpose and pronunciation of Java's volatile keyword, describes the Java Memory Model and its three guarantees, demonstrates how volatile ensures visibility but not atomicity, explores instruction reordering and memory barriers, and compares volatile with synchronized and other concurrency tools.
The author introduces himself as a Java enthusiast and states that this post serves as an introductory guide to Java concurrency, emphasizing the importance of drawing diagrams to solidify understanding.
1. How to pronounce "volatile"
The English pronunciation is shown ( ˈvɒlətaɪl for British, ˈvɑːlətl for American) and the author asks what the keyword does in Java.
2. What volatile does in Java
Provides a lightweight synchronization mechanism offered by the JVM.
Three main properties: Visibility Does not guarantee atomicity Prevents instruction reordering
To understand these properties, one must first know the Java Memory Model (JMM).
3. What is the Java Memory Model (JMM)
The JMM defines how variables are stored and accessed in two memory areas: main memory and working memory. Main memory holds the shared copies of variables, while each thread has its own working memory (stack, registers, caches).
Key rules of the JMM include:
All variables reside in main memory.
Main memory is part of the JVM's memory.
Each thread has its own working memory.
Working memory holds a copy of the variable from main memory.
All variable operations must occur in working memory.
Threads cannot directly access another thread's working memory.
Variable values are transferred between threads via main memory.
Because the JVM creates a private working memory for each thread, any read/write to a variable must first copy the value from main memory to the thread's working memory and back, ensuring visibility only when the variable is declared volatile .
4. Demonstrating visibility with volatile
A simple example shows a shared object with a field number . Without volatile , the main thread may never see the update performed by a child thread after a 3‑second sleep.
class ShareData {
int number = 0;
public void setNumberTo100() { this.number = 100; }
} public class volatileVisibility {
public static void main(String[] args) {
ShareData myData = new ShareData();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
myData.setNumberTo100();
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "ChildThread").start();
while (myData.number == 0) { /* spin */ }
System.out.println(Thread.currentThread().getName() + "\t main thread detected number != 0");
}
}When number is declared volatile , the main thread reliably detects the change.
5. Why other threads can see the update
The underlying mechanism is the cache‑coherency (snooping) protocol, typically implemented by the MESI protocol. When a CPU writes to a volatile variable, it invalidates the corresponding cache lines in other CPUs, forcing them to fetch the latest value from main memory.
6. Volatile does not guarantee atomicity
Running 20 threads that each increment a shared volatile int number 1,000 times may produce results far below the expected 20,000 because number++ consists of three separate bytecode instructions (getstatic, iadd, putstatic). Other threads can interleave between these steps.
public static volatile int number = 0;
public static void increase() { number++; }Using synchronized or AtomicInteger.getAndIncrement() ensures the final value is always 20,000.
7. Preventing instruction reordering
Volatile inserts memory barriers before and after the volatile read/write, which stops the compiler and CPU from reordering ordinary reads/writes around the volatile operation.
7.1 Memory barriers for writes
Before a volatile write, a StoreStore barrier is added; after the write, a StoreLoad barrier is added.
7.2 Memory barriers for reads
After a volatile read, a LoadLoad barrier and a LoadStore barrier are inserted.
8. Typical use case: double‑checked locking singleton
Without volatile , the instance reference may become visible before the object is fully constructed, leading to a partially initialized singleton. Declaring the instance as volatile fixes the problem.
class VolatileSingleton {
private static volatile VolatileSingleton instance = null;
private VolatileSingleton() { System.out.println(Thread.currentThread().getName() + "\t constructor"); }
public static VolatileSingleton getInstance() {
if (instance == null) {
synchronized (VolatileSingleton.class) {
if (instance == null) {
instance = new VolatileSingleton();
}
}
}
return instance;
}
}9. When to prefer volatile over synchronized
Volatile is a lightweight synchronization tool that does not block threads, making it suitable for simple status flags or one‑way communication where atomicity is not required.
10. Summary
Volatile guarantees visibility across threads.
It prevents instruction reordering in a single thread.
It does not guarantee atomicity; use synchronized or atomic classes for compound actions.
64‑bit long/double reads/writes are atomic when declared volatile.
Volatile is useful in double‑checked locking and simple flag checks.
Source code is available at https://gitee.com/jayh2018/PassJava-Learning .
Wukong Talks Architecture
Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.
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.