The volatile Keyword in Java: The Visibility Guarantee and Its Limits
-
Last Updated: September 22, 2026
-
By: javahandson
-
Series

The volatile keyword in Java fixes the visibility problem so threads never read stale values. Learn what it guarantees, why it can’t make a counter thread-safe, and when to use it.
Two threads read the same field, yet they see different values. It sounds impossible, but it happens all the time. The volatile keyword in Java exists to fix exactly this kind of surprise. It is a small word with a big job.
Here is the short version. When one thread changes a value, another thread may not notice. The second thread keeps reading an old, stale copy. Your program then behaves in ways that make no sense at all.
So what does volatile actually do about it? It makes sure every thread reads the latest value from main memory. There are no hidden copies and no stale reads. That single promise is called the visibility guarantee.
But volatile is not magic. It fixes visibility, and it stops the compiler from shuffling certain instructions. It does not make your counter thread-safe. That limit trips up many developers, so we will spend real time on it.
In this guide, we go step by step. First we will see the visibility problem in action. Then we will cover what volatile promises, what it refuses to promise, and where it fits in real code. Every example here is short and runnable.
You should know what a thread is and how to start one. You should also be comfortable reading a simple loop. If you have written a class with a couple of fields, you are ready for this.

Let us start with the mess that volatile was built to clean up. This is the visibility problem. It is the root of most confusion around threads and shared fields, so it is worth slowing down here.
Modern CPUs are fast because they avoid main memory when they can. Reading from main memory is slow, at least in CPU terms. So each core keeps recently used values in a small, quick cache that sits right next to it.
Threads run on these cores. When a thread reads a shared field, it may pull that value into its cache. After that, it happily reads the cached copy again and again. Main memory is never checked a second time.
Now picture two threads on two cores. One thread updates the field in main memory. The other thread still reads its old cached value. Neither thread is wrong on its own, yet together they disagree. That gap is the visibility problem in a nutshell.
You might wonder why the JVM allows such a mess. The answer is speed. A read from a CPU register or a local cache can be tens of times faster than a trip to main memory.
For single-threaded code, caching is a pure win. Nothing else touches your data, so a cached copy is always correct. The trouble starts only when a second thread changes the same field behind your back. Java trusts you to say when that can happen, and volatile is how you say it.
The classic example is a stop flag. One thread runs a loop. Another thread flips a boolean to tell it to quit. Sounds simple, right? Watch what goes wrong.
public class StopFlagDemo {
// no volatile here — this is the buggy version
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker started...");
while (running) {
// spin: do nothing, just keep looping
}
System.out.println("Worker stopped.");
});
worker.start();
Thread.sleep(1000); // let the worker run for a second
running = false; // ask the worker to stop
System.out.println("main set running = false");
}
}You would expect the worker to stop after one second. On many JVMs, it never does. The main thread sets running to false, but the worker keeps spinning forever. The message “Worker stopped” may never print at all.
Why does this happen? The worker read running once and cached it as true. The compiler and CPU saw no reason to read it again inside that tight loop. So the update from the main thread stays invisible to the worker.
There are two forces at play here, and both are trying to help you. Together, though, they cause this exact bug.
Neither of these is a bug in Java. They are speed tricks that shine for single-threaded code. The problem shows up only when two threads share a field without any coordination between them.
Here is the cruel part. This bug is timing-dependent, so it hides during testing. Run the same code twice and you may see two different results.
The JIT compiler kicks in only after a method runs many times. So a short test might stop correctly, while a long-running server hangs. The same code can pass on your laptop and then freeze in production. That gap is why visibility bugs are so nasty, and why volatile matters.
| 💡 Interview Insight Interviewers love the stop-flag example. If asked why a worker thread ignores a boolean change, the answer is visibility. The worker reads a cached value and never sees the update. Mention CPU caches and JIT hoisting, and add that the bug is timing-dependent — that last point shows real experience. |
Now for the fix. The volatile keyword in Java tells the JVM that a field is shared across threads. Once you mark a field volatile, a few promises kick in. Let us take them one at a time.
The first promise is visibility. A write to a volatile field goes straight to main memory. A read of a volatile field comes straight from main memory. No cached copy is trusted.
So when one thread writes, every other thread sees the new value on its next read. The stale-copy trick is switched off for that one field. This is the guarantee that fixes our stuck loop.
public class StopFlagFixed {
// one small word changes everything
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker started...");
while (running) {
// spin
}
System.out.println("Worker stopped."); // now this prints
});
worker.start();
Thread.sleep(1000);
running = false; // the worker sees this almost instantly
System.out.println("main set running = false");
}
}Add volatile to running, and the worker stops as expected. The write from main is now visible. That one keyword turned a broken program into a correct one.
Here is a promise that surprises many people. Reading or writing a plain long or double is not always atomic in Java. The value is 64 bits wide, and the JVM is allowed to move it in two 32-bit halves.
Think about what that means with two threads. One thread writes a new long. Another thread can read it after the first half lands but before the second. The reader then sees a torn value, half old and half new, which was never a real number at all.
Marking the field volatile removes this risk. A volatile long or double is always read and written as one whole unit. No thread ever sees a half-updated value.
// A plain long can be seen "torn" by another thread. private long price; // risky across threads // A volatile long is always read and written in one piece. private volatile long safePrice; // never torn
For int and boolean this is not an issue, since they fit in 32 bits. But for long and double, volatile gives you an extra guarantee for free. Keep this in mind for money and timestamps, which are often long values.
The next promise is about ordering. Compilers and CPUs are allowed to reorder instructions for speed. Usually this is harmless. Across threads, though, reordering can expose half-built work.
A volatile access acts like a fence. Writes that happen before a volatile write cannot jump after it. Reads that happen after a volatile read cannot jump before it. The field draws a line that instructions are not allowed to cross.
You do not need the deep memory-model rules to use this well. Just hold on to the plain idea. A volatile write publishes everything you did before it. A later volatile read then sees all of that published work.
A Quick Way to Picture It
Think of a volatile write as pinning a notice to a board. Everything you wrote before pinning it is now on public display. Then a volatile read is someone walking up and reading that board. They see the full notice, never a half-written one.
| 💡 Interview Insight A common interview question is whether volatile affects only the one field or more. Answer both parts. Visibility applies to that field. Ordering, though, also protects the plain writes you did before the volatile write. Mention the long/double atomicity guarantee too, and you cover the whole picture. |
Here is where many developers get burned. They assume volatile makes a field fully thread-safe. It does not. Visibility is not the same as atomicity, and that difference matters a great deal.
Let us try a shared counter. We mark it volatile and let two threads increment it. Surely the total will be correct? Run it and see.
public class VolatileCounter {
private static volatile int count = 0; // volatile, but still broken
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
count++; // looks like one step, but it is not
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// expected: 200000 — but you often get less
System.out.println("Final count = " + count);
}
}You expect 200000. Instead you get a smaller, random number on most runs. The volatile keyword did not protect the count. So what went wrong here?
The line count++ looks like one action. Under the hood, it is three separate steps. This is the crux of the whole problem.
You can even see this in the bytecode. A single count++ turns into a getstatic, an iadd, and a putstatic. Those are three distinct operations, and a thread switch can happen between any of them.
Volatile makes each read fresh and each write visible. But it does not lock the three steps together. Two threads can read the same value, both add one, and both write back the same result. One increment is simply lost.
Let us walk through one bad interleaving. Say count is 41. Both threads are about to run count++.
This is a race condition. It is not about stale reads anymore. It is about two threads stepping on the same value in the gap between read and write. Visibility cannot fix a problem that lives inside that gap.
You need atomicity, which means the read-add-write runs as one indivisible unit. Volatile does not give you that. Thankfully, Java has proper tools for the job.
The takeaway is simple. Reach for volatile when the problem is visibility. Reach for atomic classes or synchronization when the problem is a compound action. Picking the right tool is half the battle in concurrency.
| 💡 Interview Insight Expect this exact trap in interviews: “Is a volatile int counter thread-safe?” The answer is no. Explain that count++ is a read-modify-write, three steps that volatile does not bundle together. Then name AtomicInteger or a synchronized block as the real fix, and you have nailed it. |
You will hear the term happens-before whenever people discuss volatile. It sounds academic, but the core idea is easy. Let us keep it grounded and skip the heavy theory.
Happens-before is a promise about order and visibility. If action A happens-before action B, then two things are true. A finishes first, and everything A did is visible to B.
Without such a promise, the JVM is free to reorder work or hide it from other threads. With the promise, you get a guarantee you can lean on. It is the rule that makes multithreaded code predictable instead of scary.
Here is the key rule for us. A write to a volatile field happens-before every later read of that same field. This one rule ties two threads together at a single point.
So when thread A writes a volatile field, and thread B later reads it, B is guaranteed to see A’s write. Better still, B also sees everything A did before that write. The volatile field acts like a clean handoff point between the threads.
Let us make it concrete. One thread prepares some data, then sets a volatile flag. Another thread waits for the flag, then reads the data. The flag is the handoff.
public class HappensBeforeDemo {
private static int data = 0; // plain field
private static volatile boolean ready = false; // the handoff flag
public static void main(String[] args) {
// Writer thread: prepare data, then flip the flag
new Thread(() -> {
data = 42; // step 1: do the work
ready = true; // step 2: volatile write publishes it
}).start();
// Reader thread: wait for the flag, then use the data
new Thread(() -> {
while (!ready) {
// wait until the writer is done
}
// ready was true, so data = 42 is guaranteed visible
System.out.println("data = " + data); // prints 42
}).start();
}
}Notice that data itself is not volatile. Yet the reader still sees data = 42 reliably. Why? Because the volatile write to ready happens-before the reader’s read of ready, and that carries the earlier write to data along with it.
The order inside the writer thread is not just style. You must set data first and flip the flag second. Swap those two lines, and the guarantee breaks.
Say you flip ready to true before you set data. Now the reader can see ready as true while data is still zero. The handoff promise only covers work done before the volatile write. Anything after it is fair game for trouble.
That is the quiet power of happens-before. A single volatile flag can safely publish plain fields written before it. You do not need to mark every field volatile, just the handoff point at the end.
| 💡 Interview Insight If an interviewer asks how volatile helps beyond one field, bring up happens-before. Say a volatile write publishes all prior writes to any thread that later reads that field. Add that statement order matters: the plain writes must come before the volatile write. That detail sets a strong answer apart. |
So far our examples were small. Let us look at a real pattern where volatile is not optional. This is the famous double-checked locking singleton, and it shows why ordering truly matters.
A singleton is a class with only one instance. Lazy means you build that instance the first time someone asks, not before. In a threaded program, two threads might ask at the same moment.
You want to build the instance once, safely, without locking on every single call. Locking every time is correct but slow. So people reach for a check, then a lock, then a second check.
Here is the pattern with a plain field. It looks fine, and it usually runs fine. But it hides a rare, ugly bug.
public class Config {
private static Config instance; // NOT volatile — the bug is here
private Config() { /* heavy setup */ }
public static Config getInstance() {
if (instance == null) { // first check, no lock
synchronized (Config.class) {
if (instance == null) { // second check, with lock
instance = new Config(); // danger: can be reordered
}
}
}
return instance;
}
}The line instance = new Config() is not one step. The JVM allocates memory, runs the constructor, and then points instance at the object. Those steps can be reordered.
So instance might point at the memory before the constructor finishes. Another thread runs the first check, sees a non-null instance, and returns it. That thread now holds a half-built object, and its fields may still be empty. This is a real, though rare, failure.
The fix is one keyword. Mark the field volatile, and the ordering guarantee kicks in. The write to instance now happens only after the constructor is fully done.
public class Config {
private static volatile Config instance; // volatile fixes it
private Config() { /* heavy setup */ }
public static Config getInstance() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) {
instance = new Config(); // safe: fully built first
}
}
}
return instance;
}
}With volatile, no thread can see a half-built Config. The publish of the reference waits for all the setup to finish. This is the ordering promise doing real work in real code. We dig deeper into the lock itself in Article 6 — Thread Synchronization in Java.
| 💡 Interview Insight Double-checked locking is a favourite senior-level question. The key point: the singleton field must be volatile, or a thread can grab a half-constructed object due to reordering. Many candidates know the pattern but forget why volatile is there. Explain the reordering risk, and you stand out. |
By now the rule is clear. Volatile fixes visibility and ordering, not atomicity of compound actions. Let us turn that rule into a practical checklist you can use on real code.
Flags are the poster child for volatile. Think of a stop flag, a shutdown signal, or a “data is ready” marker. One thread sets it, and others read it.
The write does not depend on the current value. You just set true or false. Since there is no read-modify-write, there is no atomicity issue at all. Volatile alone does the job cleanly.
Volatile also shines when a single thread writes and many threads read. Maybe you cache a config value and refresh it now and then. The one writer updates it, and every reader sees the latest.
The rule of thumb is this. If the new value never depends on the old value, volatile is usually enough. Plain assignment is safe. It is the calculation from the old value that breaks things.
We already saw the counter fail. Any operation that reads a value and then writes a new value based on it is unsafe with volatile alone. These are called compound actions.
Each of these has a gap between reading and writing. Another thread can slip in during that gap. For these, use AtomicInteger (Article 21) or synchronization (Article 6).
Sometimes two fields must change together as a set. Imagine a range with a low and a high value, where low must always stay below high. Marking both fields volatile does not help.
A reader might catch the new low with the old high. Each field is visible on its own, but the pair is not updated as one unit. When an invariant spans several fields, you need proper locking, not volatile.
A Simple Decision Guide
| 💡 Interview Insight A sharp interview answer sounds like this: “volatile is right for a single flag or a published reference, but wrong for counters or multi-field invariants.” Tie each case to visibility versus atomicity, and you show you know the boundary, not just the keyword. |
New developers often ask how volatile stacks up against synchronized. They are not rivals. They solve different problems, and knowing the split helps you choose fast.
Volatile is lightweight. It only guarantees visibility and ordering for a single field. There is no lock, so threads never wait on each other. That makes it cheap and fast.
Synchronized is heavier, and it does more. It gives you mutual exclusion, so only one thread runs a block at a time. On top of that, it also guarantees visibility. But threads may block and wait for the lock.
The Key Differences at a Glance
So the choice is really about what you need. If you only need a fresh, visible value, pick volatile. If you need several steps to run as one safe unit, pick synchronization. We dig into locks in Article 6 — Thread Synchronization in Java.
Yes, and it is common. You might guard a compound update with a synchronized block, and still mark a separate flag volatile. They target different fields for different reasons.
One warning, though. Do not use volatile as a cheaper synchronized. If your logic needs mutual exclusion, volatile will quietly fail you. Match each tool to the exact problem in front of you.
| 💡 Interview Insight A crisp answer to “volatile vs synchronized” wins points fast. Say volatile gives visibility without a lock, while synchronized gives visibility plus mutual exclusion with a lock. Then add that volatile cannot make count++ atomic, but a synchronized block can. That contrast shows you understand both. |
A few slip-ups show up again and again. Knowing them saves you from painful, hard-to-find bugs. Let us go through the big ones.
The biggest myth is that volatile makes a field fully thread-safe. It does not. It handles visibility and ordering, and nothing more. Compound updates still need extra help.
This is the counter trap again, and it is worth repeating. A volatile counter still loses updates under load. If you are incrementing, volatile is the wrong tool. Use an atomic class instead.
The opposite mistake is just as common. Developers write a stop flag as a plain boolean and wonder why the thread never stops. A shared flag read in a loop almost always needs volatile. Add it, and the loop behaves.
This one catches even seasoned developers. A volatile array reference only makes the reference itself volatile. The elements inside the array are not volatile at all.
So a write to arr[5] gives you no visibility guarantee. Other threads may never see that change. When you need per-element visibility, reach for an atomic array type instead, which we touch on in Article 21.
Some people mark every shared field volatile to feel safe. That is wasteful and misleading. Each volatile read and write skips the cache, so it costs a little more. Use it where you truly share a field, not by default.
Volatile is not just an interview topic. It shows up in real code more than you might guess. Once you know the shape, you will spot it quickly.
Long-running services often need a clean stop. A background thread loops until a flag says otherwise. That flag is almost always volatile, so the worker sees the shutdown request right away.
Some code sets up data, then flips a flag to say it is ready. Other threads watch that flag before they read the data. The volatile flag makes the handoff safe, exactly like our earlier example.
We saw this with double-checked locking. A singleton reference is often volatile, so no thread grabs a half-built object. The same idea covers any object you build once and share widely.
Say you hold a config value that reloads now and then. One thread refreshes it, and many threads read it. A volatile reference lets every reader pick up the new value on its next read. No lock is needed for this simple swap.
A: It solves the visibility problem. A write to a volatile field goes straight to main memory, and a read comes straight from main memory. So one thread’s update is seen by every other thread on its next read. It also stops certain instruction reordering around the field.
A: Not fully. Volatile gives visibility and ordering, but not atomicity. A simple flag is safe. A counter is not, because count++ is really three steps (read, add, write) and volatile does not lock those together. For that you need an atomic class or synchronization.
A: Because count++ is a read-modify-write. Two threads can read the same value, both add one, and both write back the same result, so one increment is lost. That gap between read and write is a race condition, and visibility cannot close it. Use AtomicInteger instead.
A: Volatile guards a single field and gives visibility with no lock, so threads never wait. Synchronized guards a whole block, takes a lock, and gives both visibility and mutual exclusion, which makes the block atomic. Use volatile for a simple flag, and synchronized when several steps must run as one unit.
A: A write to a volatile field happens-before every later read of that same field. So when one thread writes the field and another reads it, the reader sees that write plus everything the writer did before it. This lets a single volatile flag safely publish plain fields written earlier.
A: Use it for a simple status flag, a shutdown signal, or a value with one writer and many readers, where the new value never depends on the old one. Avoid it for counters, check-then-act logic, or invariants that span several fields. Those need atomic classes or locks.
Let us pull it all together. The volatile keyword in Java solves the visibility problem. It forces every read and write to go through main memory, so no thread reads a stale copy.
It also gives you ordering, plus atomic reads and writes for long and double. A volatile write publishes the work you did before it, and a later volatile read sees that work. This is the happens-before promise in action.
But remember the limit. Volatile does not give you atomicity for compound actions. A counter, a toggle, or a check-then-act still needs an atomic class or synchronization. Match the tool to the problem, and your threaded code stays honest.
Reach for volatile when you share a simple flag, a published reference, or a value with one writer. Reach for stronger tools when several steps or several fields must move as one. Get that split right, and half of concurrency stops being scary.