Q21 of 40 · Core Java
Explain the role of the `volatile` keyword in concurrency.
Short answer
Short answer: `volatile` guarantees visibility: a write to a volatile variable is immediately flushed to main memory and all subsequent reads by any thread see the updated value. It prevents CPU/compiler caching of the variable in registers or thread-local caches. It does NOT provide atomicity for compound operations like increment (i++).
Detail
In a multi-core JVM, each CPU core has its own cache. Without synchronisation, a write by thread A to a variable might sit in A's cache and not be visible to thread B for an unpredictable amount of time. volatile solves the visibility problem: writes are immediately written through to main memory, and reads always go to main memory.
Happens-before: a write to a volatile variable happens-before every subsequent read of that variable by any thread. This is defined in the Java Memory Model (JMM).
What volatile does NOT provide — atomicity: the i++ operation is actually three steps: read i, increment, write i. Even if i is volatile, two threads can both read the same value, increment it independently, and produce a final count that's one short. For atomic compound operations use AtomicInteger, AtomicLong, or explicit synchronisation.
Canonical use case: a stop flag for a background thread. Without volatile, the JIT compiler might cache running in a register and never re-read from memory — the loop runs forever even after another thread sets running = false. With volatile, the write is immediately visible.
When volatile isn't enough: any "check-then-act" pattern needs synchronisation beyond volatile. if (!initialized) { initialized = true; setup(); } has a race condition even if initialized is volatile.
// EXAMPLE
VolatileExample.java
import java.util.concurrent.atomic.AtomicInteger;
// ✅ volatile for visibility: stop flag pattern
class TestWorker implements Runnable {
private volatile boolean running = true; // visible across threads
public void stop() {
running = false; // write is immediately visible to run()
}
@Override
public void run() {
while (running) { // always reads from main memory
processNextTask();
}
System.out.println("Worker stopped cleanly");
}
}
// ❌ volatile does NOT make compound ops atomic
volatile int counter = 0;
counter++; // read-increment-write: still a race condition!
// ✅ Use AtomicInteger for atomic increment
AtomicInteger atomicCounter = new AtomicInteger(0);
atomicCounter.incrementAndGet(); // single atomic CPU instruction (CAS)
atomicCounter.addAndGet(5); // atomic add
// ✅ volatile sufficient for single-write, many-read (e.g. config flag)
volatile boolean featureEnabled = false;
// One thread writes, many threads read — volatile is enough// WHAT INTERVIEWERS LOOK FOR
// COMMON PITFALL
// Related questions
What's the difference between HashMap and ConcurrentHashMap? How does ConcurrentHashMap achieve thread safety?
Core Java
Walk through the JVM memory model — heap, stack, method area, and metaspace.
Core Java
How does the volatile keyword interact with the JMM? When is it sufficient and when isn't it?
Core Java