How Does Java’s volatile Keyword Ensure Visibility and Ordering?
This article explains the two core properties of Java's volatile keyword—visibility and ordering—detailing how lock prefixes, memory barriers, and happens‑before relationships provide memory consistency, and compares volatile's implementation with synchronized blocks and CAS operations.
Introduction
volatile has two main properties: visibility and ordering.
Visibility is achieved via lock prefix, which implements a snooping mechanism that invalidates a thread’s working memory if it differs from main memory, forcing a reload.
Ordering is enforced by memory barriers that prevent instruction reordering and ensure caches are flushed.
volatile features
Single reads/writes of a volatile variable are synchronized as if protected by the same lock, e.g.
public class VolatileFeaturesExample {
volatile long vl = 0L;
public void set(long l) { vl = l; }
public void getAndIncrement() { vl++; }
public long get() { return vl; }
}Equivalent to using synchronized methods:
class VolatileFeaturesExample1 {
long vl = 0L;
public synchronized void set(long l) { vl = l; }
public synchronized long get() { return vl; }
public synchronized void getAndIncrement() {
long temp = get();
temp += 1L;
set(temp);
}
}Because the lock’s happens‑before rule guarantees memory visibility between releasing and acquiring threads, a volatile read always sees the latest write, and the lock also provides atomicity for the critical section.
Thus volatile variables have:
Visibility – a read always sees the last write.
Atomicity – single reads/writes are atomic, but compound operations like volatile++ are not.
happens‑before relationship of volatile read/write
From a memory perspective, a volatile write‑read has the same effect as a lock release‑acquire.
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a; //4
}
}
}Program order: 1 happens‑before 2, 3 happens‑before 4.
Volatile rule: 2 happens‑before 3.
Transitivity: 1 happens‑before 4.
(1) When a volatile write occurs, the JMM flushes the thread’s local copy of shared variables to main memory, making them visible to other threads.
(2) When a volatile read occurs, the thread invalidates its local copy and reads directly from main memory, synchronizing the local view.
Implementation of volatile memory semantics
JMM limits both compiler and processor reordering to preserve volatile semantics.
Before a volatile write, a StoreStore barrier prevents earlier ordinary writes from being reordered.
Before a volatile read, a LoadLoad barrier prevents later ordinary reads from moving before.
Additional barriers (StoreLoad, LoadStore) prevent reordering between volatile writes and reads.
volatile write operation
The StoreStore barrier forces all preceding ordinary writes to be flushed to main memory before the volatile write.
The following StoreLoad barrier prevents the volatile write from being reordered with subsequent volatile reads/writes.
volatile read operation
Example code:
public class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // first volatile read
int j = v2; // second volatile read
a = i + j; // ordinary write
v1 = i + 1; // first volatile write
v2 = j * 2; // second volatile write
}
}The final StoreLoad barrier cannot be omitted because the compiler cannot know whether a following volatile read or write will occur.
JVM defines memory barriers (load‑load, load‑store, store‑load, store‑store) implemented via acquire() methods.
How do volatile and CAS differ at the CPU level?
Both rely on the lock prefix, but volatile uses it as a memory barrier, while CAS uses lock cmpxchg to guarantee atomicity.
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc","memory");The lock instruction asserts the LOCK# signal, locking the bus (or cache line on modern CPUs) and preventing other CPUs from accessing memory until the instruction completes, thus acting as a memory barrier.
CAS adds lock to cmpxchg, ensuring the read‑modify‑write sequence is atomic.
Effect of the lock prefix
Early CPUs (Pentium) locked the bus; modern CPUs use cache locking, reducing overhead.
Cache locking employs the Ringbus and MESI protocol to keep caches coherent.
MESI states (Modified, Exclusive, Shared, Invalid) describe cache line status and transitions that maintain consistency across cores.
Store buffers and invalidate queues further improve performance by decoupling writes and invalidations, while read/write barriers ensure that buffered operations become visible to other cores.
Thus, lock acquisition typically includes a read barrier, and lock release includes a write barrier.
Reference: “Deep Understanding of the Java Memory Model”.
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.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
