volatile vs synchronized — Visibility vs Atomicity
Fast recall
Quick revision
Core answer
volatile access. It does NOT give atomicity of compound operations. `synchronized` gives you both mutual exclusion (only one thread in the block at a time) and visibility (happens-before on lock acquire/release). `volatile` is enough for: flags (volatile boolean running), DCL's instance reference, single-writer-multi-reader scalars, and publishing references to immutable objects. `volatile` is NOT enough for compound operations like increment (counter++ is read-modify-write, three operations), check-then-act (if (!cache.contains(k)) cache.put(k,v)), or any state involving multiple fields. For those, use synchronized, ReentrantLock, or an Atomic* class (which uses CAS internally). The biggest trap: making a counter volatile and thinking counter++ is now thread-safe. It's not.Whether the candidate understands the JMM distinction between visibility (volatile) and atomicity (synchronized), and can identify compound operations where volatile is insufficient
Assuming `volatile counter++` is atomic — it isn't. Thinking synchronized doesn't provide visibility (it does). Using volatile for multi-field state
Cite specific use cases where volatile is enough (flags, DCL, immutable publication) vs not enough (counter, compound check-then-act). Mention that on Java 21, ReentrantLock is often preferable to synchronized due to virtual-thread pinning
Interview-ready guide
Interview answer
volatile and synchronized solve overlapping but distinct problems in the Java Memory Model.
`volatile` — visibility and ordering
- Writes are flushed immediately; reads don't cache
- Establishes happens-before: writes before a volatile write are visible after the matching read
- Prevents reordering around the volatile access
- Does NOT provide atomicity for compound operations
- Non-blocking, very low overhead
`synchronized` — mutual exclusion + visibility
- Only one thread in the block at a time
- Lock acquire/release gives the same visibility as volatile
- Compound operations are safe because no interleaving
- Higher overhead under contention
- Can deadlock
- Pins virtual threads on Java 21 — use
ReentrantLockinstead for hot-path I/O
When `volatile` is enough
- Single-scalar flag:
volatile boolean running— one writer, many readers - Double-checked locking instance reference
- Publishing an immutable object reference:
volatile Config config; config = newConfig; - Last-write-wins scalars: timestamps, sequence numbers
When `volatile` is NOT enough
The canonical bug — counter increment:
volatile int counter; counter++;counter++is read → add → write, three separate operations- Two threads can both read 5 and both write 6; you lose an increment
- Fix:
AtomicInteger.incrementAndGet(), orLongAdderfor high contention, orsynchronized
Check-then-act:
if (!cache.containsKey(k)) cache.put(k, v);- Two threads both see missing, both insert; one wins, logic inconsistent
- Fix:
ConcurrentHashMap.computeIfAbsent
Multi-field consistency:
volatile int x, y; x = 1; y = 2;- Another thread can see x updated but y not yet
- Fix: synchronized block, or encapsulate
(x,y)in immutable object + volatile reference
Performance
volatile: always a few nanoseconds, never blockssynchronized: uncontended is cheap (JVM biased locking); contended is 100x worseReentrantLock: similar cost, more features (tryLock, fairness, Condition)
Decision guide
- Flag / single scalar / immutable ref →
volatile - Counter / arithmetic atomicity →
AtomicInteger/LongAdder - Compound state / check-then-act →
synchronizedorReentrantLock - Virtual threads with blocking I/O in lock →
ReentrantLock
Recommendation: Default to `volatile` when you have a single scalar and just need visibility — it's the cheapest and non-blocking. Use atomic classes (AtomicInteger, AtomicReference) for compound numeric operations; they give lock-free CAS-based atomicity. Use `synchronized` or `ReentrantLock` when you need mutual exclusion across multiple fields or complex state. On Java 21+, prefer ReentrantLock over synchronized for any hot path that performs blocking I/O, because synchronized pins virtual threads and starves carriers. The cardinal rule: whenever you find yourself thinking "this is just a counter, I'll make it volatile," stop — that's the #1 source of production bugs in this area.
`volatile` vs `synchronized` — Visibility vs Atomicity
Direct Answer
volatile gives you only visibility — writes are immediately visible to all threads, and reads/writes can't be reordered across the volatile access. It does NOT give atomicity of compound operations. synchronized gives you both mutual exclusion (only one thread in the block at a time) and visibility (happens-before on lock acquire/release). volatile is enough for: flags (volatile boolean running), DCL's instance reference, single-writer-multi-reader scalars, and publishing references to immutable objects. volatile is NOT enough for compound operations like increment (counter++ is read-modify-write, three operations), check-then-act (if (!cache.contains(k)) cache.put(k,v)), or any state involving multiple fields. For those, use synchronized, ReentrantLock, or an Atomic* class (which uses CAS internally). The biggest trap: making a counter volatile and thinking counter++ is now thread-safe. It's not.
Common Mistakes & Pitfalls
Assuming volatile counter++ is atomic — it isn't. Thinking synchronized doesn't provide visibility (it does). Using volatile for multi-field state
Check your understanding
Answer these without looking back at the article.
- Why is `volatile counter++` not thread-safe?
- What's the happens-before relationship established by `volatile`?
- When would you prefer `AtomicInteger` over `synchronized` for a counter?
- Why does `synchronized` pin virtual threads on Java 21?
- How do you publish an immutable object safely without any locking?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.