Without synchronization, there is no guarantee that a write made by one thread becomes visible to another. Each thread may keep a value in a CPU register or cache, or the compiler may hoist a field read out of a loop, so a thread can read a stale copy forever.
class Worker {
boolean stop = false; // NOT volatile
void run() {
while (!stop) { } // may loop FOREVER — stop is cached
}
void shutdown() { stop = true; } // another thread sets it
}
The while (!stop) loop can spin indefinitely even after shutdown() runs,
because the reading thread never re-reads stop from main memory. Marking
stop volatile fixes it. This is the canonical motivation for both
volatile and the Java Memory Model.
The Java Memory Model (JMM) is the part of the language spec that defines which writes a thread is guaranteed to see and what reorderings are legal. It exists because real hardware and compilers aggressively optimize — CPU caches, store buffers, out-of-order execution, and JIT reordering — so without rules, multithreaded behavior would differ per CPU and be impossible to reason about.
The JMM defines an abstract relation called happens-before: if action A
happens-before action B, then A's effects are visible to B. Anything not
ordered by happens-before may be reordered or seen stale. So the JMM doesn't
promise a single global order of operations — it promises a minimum set of
guarantees (via volatile, synchronized, final, thread start/join) that
portable concurrent code can rely on across JVMs and hardware.
Happens-before is the JMM's ordering guarantee: if A happens-before B, then everything A wrote is visible to B, and A appears to execute before B. The main sources of happens-before edges are:
| Rule | Edge |
|---|---|
| Program order | each action happens-before later actions in the same thread |
| Monitor lock | unlocking a monitor happens-before a later lock of the same monitor |
| Volatile | a write to a volatile field happens-before every later read of it |
| Thread start | t.start() happens-before any action in the started thread |
| Thread join | every action in a thread happens-before another thread's t.join() return |
| Transitivity | if A hb B and B hb C, then A hb C |
Without a happens-before edge between two threads' actions, the JMM gives no visibility or ordering guarantee — that's the heart of every concurrency bug.
volatile provides two things: visibility and ordering. A write to a
volatile field is immediately flushed so it's visible to every thread, and a
read always fetches the latest value from main memory (never a cached copy). It
also establishes a happens-before edge: a volatile write happens-before any
later volatile read of that field.
volatile boolean ready = false;
int data;
// writer thread
data = 42; // ordinary write
ready = true; // volatile write — publishes data too
// reader thread
if (ready) { // volatile read
use(data); // guaranteed to see 42
}
Because the volatile write of ready happens-before the read, the ordinary
write to data that preceded it is also visible. Rule of thumb: volatile
makes a single field a safe, lock-free flag — and piggybacks the writes before
it.
Because volatile guarantees visibility, not atomicity, and count++ is a
compound action: read, add one, write back. Two threads can both read the
same value, both increment it, and both write back the same result — a lost
update — even though every individual read and write sees fresh memory.
volatile int count = 0;
count++; // really: int tmp = count; tmp = tmp + 1; count = tmp;
Volatile makes each step visible but does not make the three steps
indivisible. For atomic counters use AtomicInteger (incrementAndGet()),
or guard the update with a lock. Rule of thumb: volatile is correct only
when a write doesn't depend on the current value (e.g. a flag), never for
read-modify-write.
Both establish happens-before edges and fix visibility, but they solve different problems:
volatile |
synchronized |
|
|---|---|---|
| Visibility | yes (per field) | yes |
| Atomicity | no | yes (the whole block) |
| Mutual exclusion | no | yes — one thread at a time |
| Blocking | never blocks | can block on the lock |
| Scope | a single field | a block / method |
volatile boolean flag; // cheap visibility for one flag
synchronized (lock) { // exclusive access + visibility
balance = balance - amount; // compound action made safe
}
Use volatile when you only need one variable's latest value visible. Use
synchronized when you need atomic compound operations or to protect
multiple related fields together.
These are three distinct concurrency concerns, and interviewers love separating them:
- Atomicity — an operation happens completely or not at all; no other thread
sees a half-finished state.
count++is not atomic. - Visibility — a write by one thread becomes observable to another. Without synchronization, writes may stay invisible (cached) indefinitely.
- Ordering — the order in which operations appear to execute. Compilers and CPUs may reorder instructions that lack a happens-before constraint.
volatile boolean v; // gives visibility + ordering, NOT atomicity
synchronized(l){...} // gives all three for the guarded region
volatile covers visibility and ordering; only locks (or atomic classes)
add atomicity. Picking the wrong tool because you conflated these is the classic
mistake.
Compilers, the JIT, and CPUs may execute instructions in a different order than written, as long as a single thread's result is unchanged. It's allowed because reordering enables huge optimizations (register allocation, hiding memory latency, out-of-order execution).
int a = 1; // these two have no dependency,
int b = 2; // so they may be reordered freely
The catch: reorderings that are invisible within one thread can be very
visible to another thread. A second thread might observe b set before a.
The JMM only forbids reordering across happens-before edges (volatile
accesses, lock release/acquire). Rule of thumb: within a thread you never
notice reordering; across threads you must create a happens-before edge to
constrain it.
The as-if-serial semantics promise that within a single thread, the program behaves as if its statements ran in exact source order — regardless of how the compiler or CPU actually reordered them. Dependencies are always respected.
int x = 5;
int y = x + 1; // depends on x — can never be reordered before it
System.out.println(y); // always 6, no matter the optimizations
So a sequential program never has to worry about reordering. The guarantee only covers one thread in isolation; the moment another thread observes the same fields, as-if-serial says nothing, and you need happens-before edges. This is why correct single-threaded code can still be broken when shared across threads.
The most common correct use of volatile: a one-way status flag that one
thread sets and others poll. Because the write/read of the flag is volatile, the
change is guaranteed visible, so the polling thread eventually stops.
class Service {
private volatile boolean running = true;
void serve() {
while (running) { doWork(); } // sees running=false promptly
}
void stop() { running = false; } // called from another thread
}
This works only because the operation is a simple assignment, not a compound update, and no other shared state needs to change atomically with it. Rule of thumb: reach for the volatile flag for cancellation/shutdown signals — and nothing that requires read-modify-write.
Double-checked locking checks the instance twice — once without a lock, once
inside — to avoid synchronizing on every call. Without volatile it is
broken because instance = new Singleton() is not atomic: it allocates
memory, runs the constructor, and assigns the reference, and those steps can be
reordered. Another thread could see a non-null reference to a
partially-constructed object.
class Singleton {
private static volatile Singleton instance; // volatile is REQUIRED
static Singleton get() {
if (instance == null) { // 1st check, no lock
synchronized (Singleton.class) {
if (instance == null) // 2nd check, locked
instance = new Singleton(); // safe publish via volatile
}
}
return instance;
}
}
volatile forbids the reordering and adds a happens-before edge, so a reader
that sees the reference also sees a fully built object. Rule of thumb: if you
hand-write DCL, the field must be volatile (or just use a holder-class
idiom instead).
Publishing an object means making it visible to other threads; safe publication ensures that when another thread sees the reference, it also sees the object's fully-initialized state. Just storing a reference in a shared field is unsafe — another thread may see the reference but stale field values.
Safe ways to publish, per Java Concurrency in Practice:
- Store it in a
volatilefield (orAtomicReference). - Store it in a
finalfield set in a constructor. - Store it into a field guarded by a lock.
- Initialize it from a static initializer.
class Holder {
private volatile Config config; // safe publication
void set(Config c) { config = c; } // readers see a complete Config
}
Rule of thumb: never just assign a shared object to a plain field and hope — route the publication through volatile, final, a lock, or a thread-safe collection.
final fields have special freeze semantics: when a constructor finishes,
all final fields are "frozen", and any thread that obtains the object through a
reference published after construction is guaranteed to see their correctly
initialized values — without additional synchronization.
class Point {
final int x, y; // frozen at end of constructor
Point(int x, int y) { this.x = x; this.y = y; }
}
// any thread seeing a Point always sees the real x and y
The one caveat: this holds only if the object doesn't leak this during
construction (e.g. registering a listener before the constructor returns). Final
fields are why immutable objects are inherently thread-safe and can be shared
freely. Rule of thumb: make fields final and the object immutable, and you
get safe sharing for free.
The JMM only guarantees that reads and writes of 32-bit-or-smaller types and
references are atomic. A non-volatile long or double (64 bits) may be
written as two separate 32-bit stores, so another thread can observe a
torn value — the high half of one write combined with the low half of
another.
long balance; // NOT volatile — write may tear
// Thread A: balance = 0xFFFFFFFF00000000L;
// Thread B might read 0xFFFFFFFF00000000, 0x0, or a mix
Declaring the field volatile makes 64-bit reads and writes atomic (and
visible). In practice most 64-bit JVMs write longs atomically anyway, but the
spec doesn't require it, so portable code must use volatile or a lock.
Rule of thumb: shared long/double that multiple threads write should be
volatile (or atomic) to avoid tearing.
A memory barrier (or fence) is a low-level CPU/compiler instruction that restricts reordering and forces cached writes to be flushed or invalidated. The JMM is the abstract model; barriers are how the JVM actually implements it on hardware.
A volatile access compiles to barriers around it:
- A volatile write is preceded by a StoreStore barrier and followed by a StoreLoad barrier — earlier writes can't move after it, and the write is flushed.
- A volatile read is followed by LoadLoad / LoadStore barriers — later reads can't move before it.
x = 1; // ordinary write
volatileFlag = 2; // StoreStore before, StoreLoad after — x can't cross down
You almost never write barriers directly in Java (they're exposed via
VarHandle); volatile and synchronized insert the right ones for you.
No. volatile on an array reference only makes the reference itself
volatile — reassigning the whole array is visible. Reads and writes of individual
elements are not volatile and carry no visibility guarantee.
volatile int[] arr = new int[10];
arr = new int[20]; // volatile: visible to other threads
arr[3] = 99; // NOT volatile — element write may be invisible
To get volatile-element semantics, use AtomicIntegerArray /
AtomicReferenceArray, or access elements through a VarHandle with the right
access mode. Rule of thumb: a volatile array gives you a safe swap of the
array, not safe concurrent element updates.
volatile operates on one field at a time, so it can't keep two related
fields consistent with each other. Even if both are volatile, another thread
can observe an update to one field but not yet the other, breaking the invariant.
class Range {
private volatile int lower = 0, upper = 10; // both volatile
void setLower(int v) {
if (v > upper) throw new IllegalArgumentException();
lower = v; // check-then-act: not atomic across two fields
}
}
Two threads calling setLower/setUpper concurrently can pass the checks and
leave lower > upper. Multi-field invariants need a lock (or an immutable
object swapped atomically). Rule of thumb: volatile is per-field; whenever
an operation spans more than one variable, you need a lock.
Use volatile when you only need a field's writes to be visible and each
operation is an independent assignment. Use an Atomic class
(AtomicInteger, AtomicReference, …) when you need an atomic
read-modify-write — increment, compare-and-set, accumulate.
volatile boolean ready; // visibility only — fine
AtomicInteger seq = new AtomicInteger();
int id = seq.incrementAndGet(); // atomic RMW — volatile can't do this
AtomicReference<Node> head = new AtomicReference<>();
head.compareAndSet(old, updated); // lock-free CAS update
Atomic classes are built on compare-and-swap (CAS) hardware instructions and
give lock-free atomicity on a single variable. Rule of thumb: flag →
volatile; counter/accumulator/CAS → Atomic; multiple fields → lock.
It provides both. People think of synchronized as a lock for mutual
exclusion, but it also creates happens-before edges that guarantee visibility:
releasing a monitor happens-before any subsequent acquire of the same
monitor. So entering a synchronized block sees all writes made before the last
thread exited it.
Object lock = new Object();
int data;
synchronized (lock) { data = 42; } // release flushes the write
synchronized (lock) { use(data); } // acquire sees 42 — visibility
That's why correctly synchronized code never needs volatile on the fields it
guards — the lock already publishes them. The two effects (exclusion +
visibility) come together, but only when both threads synchronize on the
same lock.
Transitivity — if A happens-before B and B happens-before C, then A happens-before C — is what lets a single volatile or lock edge publish a whole cluster of ordinary writes.
int a, b; // ordinary fields
volatile boolean pub;
// Writer
a = 1; // (1)
b = 2; // (2)
pub = true; // (3) volatile write
// Reader
if (pub) { // (4) volatile read
System.out.println(a + b); // sees 3
}
By program order (1) and (2) happen-before (3); the volatile rule makes (3)
happen-before (4); transitivity chains them so (1) and (2) are visible at (4) —
even though a and b are plain fields. Rule of thumb: one happens-before
edge carries along everything that preceded it, which is exactly how volatile
and locks publish data safely.
More Concurrency interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.