Understanding the Java volatile Keyword and Its Role in Concurrency
This article explains the Java volatile keyword, covering its memory visibility and ordering guarantees, how it interacts with the Java Memory Model, common pitfalls such as lack of atomicity, and practical usage examples like flag signaling and double‑checked locking in concurrent programming.
In Java interview scenarios, interviewers often start with the volatile keyword to explore a candidate's knowledge of Java concurrency, the Java Memory Model (JMM), and related JVM and operating‑system concepts.
Interviewer: How do you understand the volatile keyword?
A variable declared volatile provides two main properties:
Guarantees memory visibility across threads.
Prevents certain instruction reorderings.
Interviewer: What are memory visibility and reordering?
Java defines a memory model that abstracts away hardware and OS differences. All variables reside in main memory, while each thread has its own working memory (similar to CPU registers or caches). Threads read from main memory into working memory, operate on the copy, and write back.
The following diagram illustrates the read‑load‑execute‑write‑store cycle:
Because each thread works on its own copy, race conditions can appear, as shown by the simple increment example:
i = i + 1;If two threads execute the above code concurrently, the final value of i may be 1 instead of 2 due to stale caches.
JMM addresses three core characteristics:
Atomicity
Visibility
Ordering
While volatile directly guarantees visibility and ordering, it does not provide atomicity for compound actions such as i++ .
Interviewer: Explain the three characteristics in detail.
Atomicity
Simple reads and writes of primitive types are atomic, but operations like i++ consist of read‑modify‑write steps and are therefore not atomic. To achieve atomicity you must use synchronized , Lock , or classes from java.util.concurrent.atomic .
i = 2;
j = i;
i++;
i = i + 1;Visibility
A volatile variable is written to main memory immediately, and any subsequent read by another thread fetches the latest value from main memory, unlike ordinary variables which may stay cached.
Ordering
The JMM allows the compiler and CPU to reorder instructions, but it enforces “as‑if‑serial” semantics: the program’s observable result must remain unchanged. The volatile keyword creates a happens‑before relationship that prevents harmful reorderings.
double pi = 3.14; // A
double r = 1; // B
double s = pi * r * r; // CStatements A and B may be reordered, but C cannot move before them.
JMM also defines a set of happens‑before rules (program order, monitor lock, volatile write‑read, transitivity, thread start, join, interrupt, finalize). These rules together ensure that writes to a volatile variable become visible to other threads before they read it.
Program order rule. Monitor lock rule. Volatile variable rule. Transitivity. Thread start rule. Thread join rule. Interrupt rule. Finalize rule.
Interviewer: Does volatile guarantee atomicity?
No. It guarantees visibility and ordering for single read/write actions, but compound actions like volatile++ are still non‑atomic. The following example demonstrates the problem:
public class Test {
public volatile int inc = 0;
public void increase() { inc++; }
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(){
public void run(){
for (int j = 0; j < 1000; j++)
test.increase();
}
}.start();
}
while (Thread.activeCount() > 1) Thread.yield();
System.out.println(test.inc);
}
}Although the expected result is 10 000, the actual output is often smaller because each inc++ involves a read‑modify‑write sequence that is not atomic.
Interviewer: How is volatile implemented at the hardware level?
Compiling volatile‑annotated code adds a lock prefix to the generated assembly, which acts as a memory barrier: it prevents reordering across the barrier, forces the CPU cache to flush to main memory, and invalidates other CPUs' caches, making the new value visible to all threads.
Interviewer: Where would you use volatile? Give two examples.
Flag signaling
int a = 0;
volatile boolean flag = false;
public void write(){ a = 2; flag = true; }
public void multiply(){ if (flag) { int ret = a * a; } }Marking flag as volatile ensures that the change is immediately visible to other threads.
Double‑checked locking for a singleton
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class){
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}Here volatile prevents the JVM from reordering the instance creation, ensuring safe lazy initialization.
Overall, volatile is a lightweight tool for guaranteeing visibility and ordering in multithreaded Java programs, but developers must combine it with proper synchronization when atomicity is required.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.