Understanding Java's volatile Keyword: Purpose, Usage, and Underlying Mechanisms
This article explains the purpose, proper usage, and underlying mechanisms of Java's volatile keyword, including examples that demonstrate its role in preventing instruction reordering, ensuring visibility, and its limitations regarding atomicity, along with details on memory barriers and the Java Memory Model.
1. Purpose of volatile
While synchronized can solve visibility, ordering, and atomicity issues, it is heavyweight. The volatile keyword offers a lighter solution for visibility and ordering, though it only guarantees atomicity for single read/write operations (e.g., long and double) and not for compound actions like i++.
2. How to use volatile
2.1 Preventing Reordering
In the classic double‑checked locking singleton implementation, the volatile modifier on the instance variable prevents the JVM from reordering object construction steps, which could otherwise expose a partially initialized object to other threads.
package com.paddx.test.concurrent;
public class Singleton {
public static volatile Singleton singleton;
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}The three construction steps (memory allocation, initialization, reference assignment) may be reordered without volatile, leading to unsafe publication.
2.2 Ensuring Visibility
When one thread updates a shared variable, other threads may not see the change because each thread works with its own cache. Declaring the variable as volatile forces a write‑through to main memory and invalidates cached copies in other threads.
package com.paddx.test.concurrent;
public class VolatileTest {
int a = 1;
int b = 2;
public void change() { a = 3; b = a; }
public void print() { System.out.println("b="+b+";a="+a); }
public static void main(String[] args) {
while (true) {
final VolatileTest test = new VolatileTest();
new Thread(() -> { Thread.sleep(10); test.change(); }).start();
new Thread(() -> { Thread.sleep(10); test.print(); }).start();
}
}
}Running this code shows three possible outcomes; the unexpected b=3;a=1 occurs because the write to a is not visible to the printing thread. Declaring both a and b as volatile eliminates this case.
2.3 Atomicity Limits
Although a single read or write of a volatile long/double is atomic, compound operations like i++ are not. The following test demonstrates that volatile int i does not guarantee atomic increments, producing a final value far below the expected 1000.
package com.paddx.test.concurrent;
public class VolatileTest01 {
volatile int i;
public void addI() { i++; }
public static void main(String[] args) throws InterruptedException {
final VolatileTest01 test01 = new VolatileTest01();
for (int n = 0; n < 1000; n++) {
new Thread(() -> { Thread.sleep(10); test01.addI(); }).start();
}
Thread.sleep(10000);
System.out.println(test01.i);
}
}The result varies because i++ consists of read, modify, and write steps, none of which are protected by volatile. Use AtomicInteger or synchronized for true atomicity.
3. Underlying Mechanism of volatile
3.1 Visibility Implementation
Writes to a volatile variable flush the new value to main memory.
Subsequent reads by other threads cause their local caches to be invalidated, forcing a fresh read from main memory.
3.2 Ordering Implementation
The Java Memory Model defines a happens‑before relationship: a write to a volatile field happens before every subsequent read of that field. This prevents certain reorderings.
3.3 Memory Barriers
The JVM inserts memory‑barrier instructions to enforce the required ordering. Four types are used:
LoadLoad : ensures a later load sees the result of an earlier load.
StoreStore : ensures a later store does not become visible before an earlier store.
LoadStore : ensures a later store does not occur before an earlier load.
StoreLoad : the strongest barrier, preventing a later load from being reordered before an earlier store.
package com.paddx.test.concurrent;
public class MemoryBarrier {
int a, b;
volatile int v, u;
void f() {
int i, j;
i = a; j = b; i = v; // LoadLoad
j = u; // LoadStore
a = i; b = j; // StoreStore
v = i; u = j; // StoreStore
i = u; // StoreLoad
j = b; a = i;
}
}4. Summary
The volatile keyword is a useful optimization for specific concurrency scenarios where writes do not depend on the current value and the variable is not part of a larger invariant. It provides visibility and ordering guarantees but cannot replace synchronized for operations requiring atomicity.
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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
