Why volatile Isn't Enough: Visibility, Atomicity, and Ordering in Java
This article explains Java's volatile keyword, showing how it guarantees memory visibility and prevents instruction reordering, but does not provide atomicity, illustrated with code examples and a discussion of the Java Memory Model and double‑checked locking.
Introduction
volatile is a lightweight synchronization mechanism provided by Java, offering a lighter‑weight alternative to synchronized for certain concurrency scenarios.
Visibility Issue Example
The following program creates a shared boolean status flag that is initially false. A worker thread loops while !status, while the main thread sleeps for 100 ms, sets status = true, and prints the flag.
package com.secbro2.others.testVolatile;
public class TestVolatile {
private static boolean status = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!status) {
}
}).start();
Thread.sleep(100L);
status = true;
System.out.println("status is " + status);
}
}Even though the output shows "status is true", the worker thread continues looping because the change to status is not visible to the other thread.
Memory Visibility
In a multithreaded environment, a shared variable may become "invisible" to other threads if updates are not propagated, leading to stale reads.
Java Memory Model (JMM)
The JMM defines an abstract relationship between main memory and each thread's local memory. Shared variables reside in main memory; each thread works on a private copy in its local memory. All reads and writes must go through this working memory, not directly to main memory.
How volatile Provides Visibility
When a volatile variable is written, the JMM forces the thread's local cache to flush the new value to main memory.
The write also invalidates the cached copies of that variable in other threads, forcing them to read the fresh value.
Volatile and Atomicity
Atomicity is another concurrency concern. The following example increments a volatile int inc from ten threads, each performing 1 000 increments.
package com.secbro2.others.testVolatile;
public class Counter {
private volatile int inc = 0;
private void increase() { inc++; }
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increase();
}
}).start();
}
Thread.sleep(3000L);
System.out.println(counter.inc);
}
}The program prints a value such as 6847 instead of the expected 10000, demonstrating that volatile guarantees visibility but not atomicity. The increment operation consists of three steps (read, add, write) that can interleave across threads.
Using synchronized (or AtomicInteger) makes the increment atomic and yields the correct result of 10000.
Volatile and Ordering
Processors may reorder instructions for performance, but volatile prevents such reordering around the volatile read/write.
int a = 10; // statement 1
int r = 2; // statement 2
a = a + 3; // statement 3
r = a * a; // statement 4Possible execution order could be 2 → 1 → 3 → 4, but not 2 → 1 → 4 → 3. Volatile ensures that operations before a volatile read/write are completed and visible before subsequent operations proceed.
When a volatile read or write occurs, all prior actions are guaranteed to have been performed and made visible to later actions.
During optimization, the compiler and CPU cannot move statements across a volatile access.
Double‑Checked Locking Singleton with volatile
The classic double‑checked locking pattern requires a volatile instance field to prevent reordering.
package com.secbro2.gof23.singleton;
public class SingletonThreadSafe2 {
private static volatile SingletonThreadSafe2 instance;
private SingletonThreadSafe2() {}
public static SingletonThreadSafe2 getInstance() {
if (instance == null) {
synchronized (SingletonThreadSafe2.class) {
if (instance == null) {
instance = new SingletonThreadSafe2();
}
}
}
return instance;
}
public void helloSingleton() {
System.out.println("Hello SingletonThreadSafe1!");
}
}Conclusion
volatile provides a lightweight way to ensure that writes become visible to other threads and to prevent certain instruction reorderings, making it suitable for flags and the double‑checked locking pattern. However, it does not guarantee atomicity; for compound actions you still need synchronized blocks, atomic classes, or other stronger synchronization mechanisms.
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.
Senior Brother's Insights
A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.
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.
