The volatile keyword makes every write to a field visible to all threads that read the field afterwards, but it does not make compound updates such as count++ atomic. So a volatile field is thread safe for a flag or a reference that one thread sets and others read, and it is not thread safe for a counter that several threads update.
We use volatile for stop flags, status values and configuration objects that get replaced as a whole, and for the instance field in double-checked locking.
The following example stops a background import job from the main thread. The ImportWorker class is shown in section 2.
ImportWorker worker = new ImportWorker();
Thread importer = Thread.ofPlatform().start(worker);
LockSupport.parkNanos(Duration.ofMillis(50).toNanos()); // let it work for a moment
worker.stop(); // volatile write
importer.join();
boolean stopped = !importer.isAlive(); // true
The loop inside the worker reads the volatile field on every pass, so it sees the change and the thread ends. We cover the visibility rule behind it, lost updates, publishing objects safely, double-checked locking and how volatile compares with locks and atomic classes.
1. What the volatile Keyword Guarantees
Without any synchronization, the Java memory model lets the JIT compiler keep a field’s value in a register, and it lets the compiler and the CPU reorder reads and writes when the result looks the same to the current thread. Another thread can therefore keep reading an old value of a field for a long time, or see writes in a different order than they appear in the code.
A volatile field turns off those optimizations for that field. The specification expresses it as a happens-before rule, where a write to a volatile field happens before every later read of the same field.

The rule also covers the writes that come before the volatile write. If Thread A sets data = 42 before it sets ready = true, a thread that reads ready as true also sees data as 42, even though data itself is not volatile.
- Visibility means that a read of a volatile field returns the latest write to it by any thread.
- Ordering means that writes before a volatile write cannot move after it, and reads after a volatile read cannot move before it.
- Atomicity applies to single reads and writes only, which also covers long and double fields.
2. Stopping a Worker Thread With a volatile Flag
Say an admin page has a Cancel button for a long-running product import. The request thread sets a flag, and the import thread checks it between records. The flag has one writer and its new value does not depend on the old one, which is the case volatile handles well.
class ImportWorker implements Runnable {
private volatile boolean running = true;
private int imported;
@Override
public void run() {
while (running) { // fresh read on every pass
imported++; // only this thread writes it
LockSupport.parkNanos(Duration.ofMillis(1).toNanos());
}
}
void stop() {
running = false;
}
int imported() {
return imported;
}
}
If we remove volatile, the JIT compiler may read running once and turn the loop into an endless one, so the worker never stops. The bug depends on the JVM and the hardware, which is why it often passes tests and appears only in production.
For a worker that blocks in I/O, a queue or a lock, a flag is not enough because the thread never reaches the check. In those cases we call interrupt() on the thread, or shutdownNow() on the ExecutorService that runs it.
3. Visibility Is Not Atomicity
The statement count++ is three steps, namely a read, an addition and a write. Two threads can read the same value, both add one, and both write the same result, so one increment is lost. Marking the field volatile makes each read and each write visible, yet it does nothing to stop the two threads from interleaving the steps.
class PageCounter {
volatile int views; // visible, but ++ is not atomic
final AtomicInteger atomicViews = new AtomicInteger();
}
PageCounter counter = new PageCounter();
Runnable visit = () -> {
for (int i = 0; i < 1_000_000; i++) {
counter.views++;
counter.atomicViews.incrementAndGet();
}
};
Thread visitor1 = Thread.ofPlatform().start(visit);
Thread visitor2 = Thread.ofPlatform().start(visit);
visitor1.join();
visitor2.join();
int volatileViews = counter.views; // less than 2000000 in most runs
int atomicTotal = counter.atomicViews.get(); // 2000000
A volatile field is safe only when each write does not depend on the value the same thread read before it. For counters we use AtomicInteger, which performs the read and the write as one compare-and-swap step, or LongAdder when many threads update the value often.
4. Publishing an Immutable Object Through a volatile Field
A configuration that changes at runtime is a good fit for a volatile reference. A background task loads the new settings into a new immutable object and swaps the reference in one write, so readers always see either the complete old settings or the complete new ones.
record MailConfig(String host, int port, List<String> admins) {
MailConfig {
admins = List.copyOf(admins); // immutable copy
}
}
class ConfigHolder {
private volatile MailConfig current = new MailConfig("localhost", 25, List.of("lokesh"));
MailConfig get() {
return current;
}
void reload(MailConfig fresh) {
current = fresh; // one atomic reference write
}
}
ConfigHolder holder = new ConfigHolder();
Thread reloader = Thread.ofPlatform().start(() -> holder.reload(new MailConfig("smtp.local", 587, List.of("lokesh", "alex"))));
reloader.join();
int port = holder.get().port(); // 587
int adminCount = holder.get().admins().size(); // 2
The pattern works because the object behind the reference never changes after it is published. If readers could modify MailConfig, or if reload() had to read the old value to build the new one from several threads, we would need AtomicReference.updateAndGet() or a lock.
5. volatile in Double-Checked Locking
Double-checked locking creates an expensive object lazily and takes a lock only on the first calls. Without volatile, another thread can see a non-null reference to an object whose constructor has not finished, because the write of the reference may become visible before the writes of the object’s fields.
class TemplateEngine {
private static volatile TemplateEngine instance;
private final Map<String, String> templates;
private TemplateEngine() {
templates = Map.of("welcome", "Hello {name}"); // expensive in real code
}
static TemplateEngine getInstance() {
TemplateEngine local = instance; // one volatile read
if (local == null) {
synchronized (TemplateEngine.class) {
local = instance;
if (local == null) {
local = new TemplateEngine();
instance = local; // publishes a fully built object
}
}
}
return local;
}
String template(String name) {
return templates.get(name);
}
}
TemplateEngine engine1 = TemplateEngine.getInstance();
TemplateEngine engine2 = TemplateEngine.getInstance();
boolean sameEngine = engine1 == engine2; // true
String welcome = engine1.template("welcome"); // "Hello {name}"
The pattern has worked correctly since Java 5, when JSR 133 defined the current volatile semantics. The local variable saves a second volatile read on the fast path.
For a static singleton, the holder class idiom is shorter and needs no volatile at all. The JVM initializes the nested class on first use, and class initialization is thread safe. The singleton pattern article compares more variants.
class ReportCache {
private ReportCache() {
}
private static class Holder {
static final ReportCache INSTANCE = new ReportCache();
}
static ReportCache getInstance() {
return Holder.INSTANCE;
}
}
boolean sameCache = ReportCache.getInstance() == ReportCache.getInstance(); // true
6. volatile vs synchronized vs Atomic Classes
The three tools solve different problems, and picking the wrong one is the usual source of bugs. The synchronized keyword gives both mutual exclusion and visibility, whereas volatile gives visibility only.
| Need | volatile | synchronized / Lock | Atomic classes |
|---|---|---|---|
| Visibility of the latest value | Yes | Yes | Yes |
| Atomic count++ or put-if-absent | No | Yes | Yes, for one variable |
| Keeps several fields consistent | No | Yes | No |
| Threads wait for each other | Never | Yes, under contention | No, they retry |
| Applies to | Fields only | Methods and blocks | One value or array |
A short way to decide is to ask who writes the value. One writer, or writes that replace the value without reading it, call for volatile. Several writers that update one value call for an atomic class, and updates that span several fields call for a lock.
7. volatile in Current Java
The keyword has not changed since Java 5, though the code around it has. Virtual threads follow the same memory model, so a volatile flag works the same way for them. The AtomicIntegerArray class, and since Java 9 the VarHandle class, give volatile access to single array elements, which the keyword cannot do.
AtomicIntegerArray slots = new AtomicIntegerArray(3); // volatile semantics per element
slots.set(1, 7);
int slot = slots.get(1); // 7
Most application code reaches the same guarantees through higher-level classes. A ConcurrentHashMap, a BlockingQueue or a CompletableFuture already publishes values safely between threads, so explicit volatile fields belong mostly in small flags and in library code. The Java concurrency guide groups those classes by task.
8. Java volatile FAQs
Most questions about volatile ask where visibility ends and atomicity begins.
8.1. Is a volatile variable thread safe?
Only for single reads and writes. A volatile flag or reference with one writer is thread safe, whereas count++, if (x == null) x = … and other sequences that read a value and write a new one are not.
8.2. Are writes to volatile long and double fields atomic?
Yes. For plain long and double fields, the JLS allows a 64-bit write to happen as two 32-bit writes, so another thread could see half of the value. Declaring the field volatile rules that out. HotSpot on 64-bit platforms writes them atomically anyway, but the code should not depend on it.
8.3. Are the elements of a volatile array volatile?
No. Only the array reference is volatile, so replacing the whole array is visible, but writing arr[i] is a plain write. We use AtomicIntegerArray, AtomicReferenceArray or a VarHandle for element access.
8.4. Can a local variable or a method be volatile?
No, the compiler rejects both. The modifier applies only to fields, because local variables are never shared between threads in the first place, as the local variables guide explains.
8.5. Is volatile faster than synchronized?
On x86 CPUs, a volatile read costs about as much as a normal read, and a volatile write costs more because of the memory barrier it needs. Threads never block on volatile, which is where the gain over a contended lock comes from.
9. Conclusion
A volatile field gives visibility and ordering, so a write by one thread is seen by every later read, together with the writes that came before it. It does not give atomicity for read-modify-write operations, which is why volatile counters lose updates.
We use it for stop flags, status values, immutable objects that get replaced as a whole, and the instance field in double-checked locking. For counters we pick an atomic class, and for updates that span several fields we pick a lock.
10. References
- JLS 25, volatile Fields
- JLS 25, Happens-before Order
- JLS 25, Non-Atomic Treatment of double and long
- JSR 133, Java Memory Model and Thread Specification Revision
- VarHandle (Java SE 25 API)
Happy Learning !!