Why Volatile Isn’t Enough: Mastering Atomicity and the Java Memory Model
This article explains the concept of atomicity, demonstrates atomic and non‑atomic Java code using volatile and AtomicInteger, discusses visibility, instruction reordering, the happens‑before principle, and how the JVM implements its memory model with heap and stack structures.
Atomicity and Its Meaning
Atomicity means an operation is performed as a single, indivisible step that cannot be interrupted; it either completes fully or not at all.
Example of Atomic Operations
<code>class Data{
AtomicInteger atomicInteger = new AtomicInteger();
volatile int number = 0;
public void numberIncrement(){
this.number++;
}
public void atomicIntegerIncrement(){
this.atomicInteger.incrementAndGet();
}
}
public class Main {
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i++){
new Thread(() -> {
for(int j = 0; j < 1000; j++){
data.numberIncrement();
data.atomicIntegerIncrement();
}
}, "t"+i).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("volatile int:" + data.number);
System.out.println("AtomicInteger:" + data.atomicInteger);
}
}
</code>Non‑Atomic Example
<code>class Data{
volatile int number = 0;
public void numberIncrement(){
this.number++;
}
}
public class Main {
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i++){
new Thread(() -> {
for(int j = 0; j < 1000; j++){
data.numberIncrement();
}
}, "t"+i).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(data.number);
}
}
</code>The program aims to let ten threads increase number to 10,000, but because volatile does not guarantee atomicity, the result is unpredictable. The output image below shows the actual result.
Both operations in the first example are atomic, meaning they execute in order without interruption; they either both succeed or both fail.
Visibility and volatile
Visibility means that when one thread modifies a shared variable, other threads see the updated value immediately. volatile ensures that writes are flushed to main memory and reads fetch the latest value, but it does not provide mutual exclusion.
How to Make It Thread‑Safe
Condition 1: The computation does not depend on the current value of the variable, or only one thread modifies the variable.
Condition 2: The variable does not need to participate in invariants with other state variables.
Ordering guarantees that operations observed within a single thread appear in program order; the volatile and synchronized keywords can be used to enforce this across threads.
Instruction Reordering
Compilers and processors may reorder instructions for performance, but the Java Memory Model (JMM) inserts memory barriers to preserve the happens‑before relationship.
In single‑threaded code, reordering does not affect the final result, but in multithreaded scenarios it can cause unexpected behavior, such as executing line 1, then line 3, then line 2.
Happens‑Before Principle
The happens‑before relationship describes visibility between two actions; if one action happens‑before another, the second must see the effects of the first, which is essential for determining thread safety.
<code>int a = 10;
b = b + 1;
</code>When an operation depends on a previous value (e.g., b = b + a ), the compiler cannot reorder it because of the data dependency.
JVM Implementation of the Memory Model
The JVM memory model consists of two main areas: the thread stack and the heap.
Each thread has its own stack (call stack) that stores method call information.
Communication between threads requires two steps: a thread writes a shared variable and flushes it to main memory; another thread reads the updated value from main memory.
In the JVM, each thread may have a private copy of a variable; updates are synchronized via the main memory to ensure visibility.
Heap vs. Stack
The heap is a runtime data area for dynamically allocated objects, supporting garbage collection but with slower access than the stack.
The stack provides fast access for primitive types and object references but has a fixed size and limited flexibility.
When multiple threads access an object's fields, each thread works with its own copy of the field in its local memory.
The Java memory model differs from hardware memory; hardware does not distinguish between heap and stack, but the JVM defines these abstractions to manage memory consistency.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.