Why volatile Doesn’t Guarantee Atomicity in Java and How to Use It Correctly
This article explains the Java memory model, the role of the volatile keyword in ensuring visibility and ordering, why it cannot provide atomicity for compound operations, and presents proper synchronization techniques such as synchronized, Lock, AtomicInteger, and double‑checked locking.
Memory Model Basics
The Java Memory Model (JMM) abstracts differences between hardware and OS memory access, defining how variables are read and written across threads, with each thread having its own working memory (cache) and a shared main memory.
Cache Consistency and Visibility Issues
When multiple threads operate on shared variables, caches may become inconsistent, leading to stale values. Two hardware‑level solutions exist: bus locking (LOCK#) and cache‑coherence protocols such as Intel's MESI.
Three Core Concurrency Concepts
Atomicity
Atomicity means an operation executes completely without interruption. Simple reads and writes of primitive types are atomic, but compound actions like i++ or i = i + 1 are not.
Visibility
Visibility ensures that a write by one thread becomes immediately observable by others. Declaring a variable volatile forces writes to main memory and invalidates other CPUs' cache lines.
Ordering
Ordering guarantees that operations appear in program order. The JMM allows instruction reordering for performance, but volatile prevents reordering around its accesses and the JVM enforces a happens‑before relationship.
Happens‑Before Rules
Program order rule: earlier statements happen before later ones in a single thread.
Lock rule: an unlock happens before a subsequent lock on the same monitor.
Volatile rule: a write to a volatile variable happens before any later read of that variable.
Thread start rule: Thread.start() happens before any actions in the started thread.
Thread termination rule: actions in a thread happen before another thread detects its termination via join() or isAlive().
Why volatile Does Not Provide Atomicity
Even though volatile guarantees visibility, operations like inc++ involve a read‑modify‑write sequence that can interleave between threads, producing lost updates. Therefore, volatile alone cannot ensure correct results for incrementing counters.
Correct Synchronization Techniques
To achieve atomicity, use one of the following:
synchronized blocks or methods.
Lock implementations such as ReentrantLock.
AtomicInteger and other classes from java.util.concurrent.atomic that use CAS.
When to Use volatile
Use volatile only when writes do not depend on the current value and the variable is not part of a larger invariant. Typical scenarios include state flags, double‑checked locking for lazy initialization, and simple status indicators.
Double‑Checked Locking Example
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}This pattern relies on volatile to prevent reordering of the instance reference assignment.
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.
