volatile Keyword
volatile Keyword
volatile guarantees that a write to a field by one thread becomes visible to every other thread that reads it — no thread ever sees a stale, cached copy. What it does not do is make a compound operation like count++ atomic, a distinction that trips up even experienced developers.
What Does volatile Guarantee?
volatile is a field modifier that gives one specific promise: every write to that field is guaranteed to become visible to every thread that subsequently reads it. It says nothing about atomicity, and nothing about coordinating multiple related fields — only that a single field's value is never served stale from a per-thread cache.
Why Thread Safety Matters
A plain field gives the JVM no obligation to make a write visible to another thread promptly, or ever — a worker thread's loop checking a non-volatile flag is legally permitted to never notice it changed. That single gap is responsible for a whole class of concurrency bugs that never show up in a quick test but surface reliably once JIT optimization and real load are involved.
volatile fixes visibility, not atomicity. A volatile int with count++ on it is still not thread-safe — the read, increment, and write-back are still three separate steps.
1// File: BeforeVolatile.java
2// Illustrative only - do not run this to completion. Without volatile, the
3// JVM is legally permitted to let the worker thread's loop never observe
4// the write to stop, since nothing here establishes a happens-before edge.
5
6public class BeforeVolatile {
7
8 static boolean stop = false;
9
10 public static void main(String[] args) throws InterruptedException {
11 Thread worker = new Thread(() -> {
12 long iterations = 0;
13 while (!stop) {
14 iterations++;
15 }
16 System.out.println("Worker stopped after seeing the flag");
17 });
18
19 worker.start();
20 Thread.sleep(100);
21 stop = true;
22 }
23}Marking the field volatile guarantees the write becomes visible, so the worker's loop is certain to eventually see it and exit.
1// File: AfterVolatile.java
2
3public class AfterVolatile {
4
5 static volatile boolean stop = false;
6
7 public static void main(String[] args) throws InterruptedException {
8 Thread worker = new Thread(() -> {
9 long iterations = 0;
10 while (!stop) {
11 iterations++;
12 }
13 });
14
15 worker.start();
16 Thread.sleep(100);
17 stop = true;
18 worker.join();
19
20 System.out.println("Worker stopped after seeing the flag");
21 }
22}Output:
Worker stopped after seeing the flag
volatile does not promise the worker will see the change instantly, only that it is guaranteed to see it eventually — which is exactly why join() in the fixed version is guaranteed to return rather than wait forever.
How It Works
One sentence before the diagram: a volatile write is what forces a value out of a CPU core's local cache and into main memory, where every other core is guaranteed to see it.
Thread A (writer) Thread B (reader)
| |
running = true |
| |
volatile write --- happens-before --> volatile read
| |
v v
main memory updated guaranteed to see
immediately, not just running == true, not a
cached in Thread A's CPU core stale cached value
That happens-before edge is the entire mechanism — the Java Memory Model guarantees that a write to a volatile field happens-before every subsequent read of that same field by any thread, and synchronized establishes the identical guarantee on entry and exit from a locked block.
Reading, incrementing, and writing back a field's value is three separate steps, and volatile guarantees visibility of each individual step — not that the whole sequence happens as one uninterruptible unit. Two threads can both read the same value before either writes its incremented result back, silently losing an update.
1// File: VolatileNotAtomicExample.java
2// Illustrative only - do not rely on this for a correct count. volatile
3// guarantees each thread sees the latest value, but the read-increment-write
4// sequence of ++ is still not a single atomic operation, so the final total
5// is not reliably 40000 - it varies from run to run.
6import java.util.concurrent.CountDownLatch;
7
8public class VolatileNotAtomicExample {
9
10 static volatile int counter = 0;
11
12 public static void main(String[] args) throws InterruptedException {
13 int threadCount = 4;
14 CountDownLatch done = new CountDownLatch(threadCount);
15
16 for (int i = 0; i < threadCount; i++) {
17 new Thread(() -> {
18 for (int j = 0; j < 10_000; j++) {
19 counter++;
20 }
21 done.countDown();
22 }).start();
23 }
24
25 done.await();
26 System.out.println("Expected 40000, got: " + counter);
27 }
28}AtomicInteger, covered in full in this section's dedicated Atomic Variables article, is what actually fixes this — its incrementAndGet() performs the entire read-modify-write sequence as one atomic operation.
1// File: AtomicFixExample.java
2import java.util.concurrent.CountDownLatch;
3import java.util.concurrent.atomic.AtomicInteger;
4
5public class AtomicFixExample {
6
7 static AtomicInteger counter = new AtomicInteger();
8
9 public static void main(String[] args) throws InterruptedException {
10 int threadCount = 4;
11 CountDownLatch done = new CountDownLatch(threadCount);
12
13 for (int i = 0; i < threadCount; i++) {
14 new Thread(() -> {
15 for (int j = 0; j < 10_000; j++) {
16 counter.incrementAndGet();
17 }
18 done.countDown();
19 }).start();
20 }
21
22 done.await();
23 System.out.println("Expected 40000, got: " + counter.get());
24 }
25}Output:
Expected 40000, got: 40000
Code Examples
Double-checked locking, the classic pattern for lazily initializing a singleton without paying synchronization overhead on every call after the first, is the case where skipping volatile looks completely fine in testing and still breaks in production.
1// File: DoubleCheckedLockingExample.java
2
3public class DoubleCheckedLockingExample {
4
5 private static volatile DoubleCheckedLockingExample instance;
6
7 private DoubleCheckedLockingExample() {}
8
9 public static DoubleCheckedLockingExample getInstance() {
10 if (instance == null) {
11 synchronized (DoubleCheckedLockingExample.class) {
12 if (instance == null) {
13 instance = new DoubleCheckedLockingExample();
14 }
15 }
16 }
17 return instance;
18 }
19
20 public static void main(String[] args) {
21 DoubleCheckedLockingExample first = getInstance();
22 DoubleCheckedLockingExample second = getInstance();
23
24 System.out.println("Same instance: " + (first == second));
25 }
26}Output:
Same instance: true
Without volatile on instance, another thread could observe a reference to a partially-constructed object, due to how the JVM is permitted to reorder the steps of object construction and field assignment — volatile prevents exactly that reordering from becoming visible. A stop or shutdown flag for signaling a background loop to exit, and publishing a simple single-writer value that other threads only read, are the other two situations where volatile alone is the right tool, without needing full synchronized mutual exclusion.
Real-World Example
A background price-feed poller runs on its own thread until signaled to stop, using a volatile flag to make that signal reliable regardless of JIT optimization or which CPU core each thread happens to run on.
1// File: PriceFeedPoller.java
2
3public class PriceFeedPoller implements Runnable {
4 private volatile boolean running = true;
5 private int pollCount = 0;
6
7 @Override
8 public void run() {
9 while (running) {
10 pollCount++;
11 }
12 }
13
14 public void stop() {
15 running = false;
16 }
17
18 public int getPollCount() {
19 return pollCount;
20 }
21}1// File: PriceFeedPollerDemo.java
2
3public class PriceFeedPollerDemo {
4 public static void main(String[] args) throws InterruptedException {
5 PriceFeedPoller poller = new PriceFeedPoller();
6 Thread pollerThread = new Thread(poller);
7
8 pollerThread.start();
9 Thread.sleep(100);
10 poller.stop();
11 pollerThread.join();
12
13 System.out.println("Poller stopped: " + !pollerThread.isAlive());
14 System.out.println("Polled at least once: " + (poller.getPollCount() > 0));
15 }
16}Output:
Poller stopped: true
Polled at least once: true
pollCount deliberately is not volatile or atomic — it is only ever written by the single poller thread, and main only reads it after join(), which already establishes a happens-before edge on its own, so no additional guarantee is needed for that specific field. A mistake that appears often in fresher pull requests is declaring a stop flag as a plain boolean field and assuming a worker thread's loop will notice the change promptly, when nothing about a plain field guarantees the write is ever seen by another thread at all. Marking running as volatile, exactly as PriceFeedPoller does here, is what makes stop() actually reliable rather than something that happens to work by coincidence on a given JVM and workload.
Best Practices
Use volatile for a simple flag or a single-writer value where only visibility is needed, not mutual exclusion over a compound operation.
Reach for AtomicInteger, AtomicLong, or AtomicReference instead of volatile the moment a field needs a read-modify-write operation like incrementing, rather than a simple assignment.
Apply volatile to the singleton reference itself in double-checked locking — without it, the pattern is unsafe regardless of how correct the rest of the code looks.
Do not treat volatile as a lighter-weight substitute for synchronized in general — it solves a narrower problem, visibility of individual reads and writes, not coordination across multiple related fields or operations.
Common Mistakes
Assuming a volatile field makes a check-then-act sequence safe overlooks that visibility of each individual read and write does not make the sequence between them atomic — two threads can both read the same value before either writes back the result of acting on it.
1// Illustrative only - a volatile field does not make check-then-act atomic.
2// Two threads can both read available == 1 before either writes 0, letting
3// both believe they successfully reserved the last unit.
4public class VolatileCheckThenActMistake {
5 static volatile int available = 1;
6
7 static boolean tryReserve() {
8 if (available > 0) {
9 available--;
10 return true;
11 }
12 return false;
13 }
14}Assuming volatile protects a mutable object's internal state is a second, subtler mistake — volatile on a reference field only guarantees visibility of the reference itself being reassigned, not of changes made to the fields of the object that reference currently points to. A volatile List<String> items field still needs its own synchronization, or a genuinely thread-safe collection, to safely mutate items concurrently — reassigning items to a whole new list is the only operation volatile alone makes safe here.
Interview Questions
Q1. What guarantee does volatile provide, and what does it NOT provide?
It guarantees visibility — a write to a volatile field by one thread is guaranteed to eventually be seen by any other thread that reads it. It does not provide atomicity for compound operations like count++, and it does not provide mutual exclusion the way synchronized does. The nuance interviewers are listening for is whether you can name both halves without prompting.
Q2. Why does a non-volatile stop flag risk an infinite loop in a worker thread?
Without volatile, nothing obligates the JVM to make a write to the flag visible to another thread — the reading thread's loop is legally permitted to keep observing a stale, cached value indefinitely, exactly as illustrated in this article's BeforeVolatile example.
Q3. Is volatile int counter; counter++; thread-safe?
No. ++ is a read-modify-write sequence of three separate steps, and volatile only guarantees visibility of each step individually — two threads can still both read the same value before either writes back its increment, losing an update, exactly as illustrated in this article's VolatileNotAtomicExample. This is the single most common trick question this topic produces.
Q4. Why is volatile important in the classic double-checked locking singleton pattern?
Without it, another thread could observe a reference to a partially-constructed object, since the JVM is permitted to reorder the steps of construction relative to the field assignment in the absence of volatile. Marking the singleton reference volatile prevents that reordering from ever becoming visible to another thread. Product-company interviewers often use this to test whether a candidate actually understands instruction reordering, not just the pattern's shape.
Q5. What is happens-before, and how does volatile establish it?
Happens-before is the Java Memory Model's guarantee that one action's effects are visible to another action that follows it. A write to a volatile field happens-before every subsequent read of that same field by any thread, which is precisely the mechanism that makes visibility reliable.
Q6. Does synchronized also provide the visibility guarantee volatile provides?
Yes. Entering and exiting a synchronized block also establishes happens-before, giving it the same visibility guarantee volatile provides, in addition to the mutual exclusion volatile does not provide.
Q7. Can a volatile field prevent a check-then-act race condition?
No, as demonstrated in this article's Common Mistakes section — volatile guarantees each individual read and write is visible, but it does not make the check and the subsequent act a single atomic operation, so two threads can still both pass the check before either updates the value.
FAQs
Is volatile a replacement for synchronized?
No. It solves a narrower problem — visibility of individual reads and writes — while synchronized also provides mutual exclusion for compound operations. They are complementary tools, not interchangeable ones.
Does volatile affect performance?
Yes, modestly — a volatile read or write cannot be freely reordered or cached in a CPU register the way an ordinary field access can, which has a small but real cost. It is far cheaper than synchronized, however, since it involves no locking or thread blocking.
Can volatile be applied to a method or a class?
No, only to fields. There is no volatile method or class modifier in Java.
Does marking a reference field volatile also make the object it points to thread-safe?
No. volatile on a reference field only guarantees visibility of the reference itself changing — mutating the internal state of the object that reference points to still needs its own synchronization, exactly as covered in this article's Common Mistakes section.
Is AtomicInteger's internal value volatile?
Yes — internally, AtomicInteger and the other atomic classes store their value in a volatile field, then layer compare-and-swap operations on top of it to also guarantee atomicity, which a plain volatile field on its own does not provide.
Does final provide any of the same guarantees as volatile?
Related but different — a final field has a safe-publication guarantee: once an object's constructor finishes, any thread that obtains a reference to the object is guaranteed to see its final fields correctly initialized, without needing volatile or further synchronization. This applies specifically to construction, not to ongoing visibility of later changes the way volatile does.
Is it necessary to use volatile for fields only ever accessed by one thread?
No. volatile's entire purpose is making writes visible to other threads — a field never read or written by more than one thread has no visibility problem to solve in the first place.
Summary
volatile guarantees that a write to a field becomes visible to every thread that subsequently reads it, closing the exact gap that lets a worker thread's loop ignore a plain field's change indefinitely. What it does not do is make a compound operation like incrementing, or a check-then-act sequence, atomic — that guarantee belongs to AtomicInteger and the rest of the atomic classes, or to synchronized.
The habit worth carrying forward from this article's price-feed poller example is reaching for volatile specifically for simple flags and single-writer values, and reaching for an atomic class or synchronized the moment more than a single plain read or write is involved in the operation that needs to be safe.
What to Read Next
Learn what happens when two threads update data at once.