What is the difference between synchronized and volatile
The synchronized and volatile keywords address two fundamentally different aspects of multi-threaded coordination in Java:
🟢 Junior Level
30-Second Summary
The synchronized and volatile keywords address two fundamentally different aspects of multi-threaded coordination in Java:
volatilesolves the visibility problem and prevents CPU instruction reordering. Writes to avolatilevariable are immediately flushed across CPU caches, making the update instantly visible to all other threads. However,volatiledoes not provide atomicity for compound read-modify-write operations (such ascount++).synchronizedsolves three problems simultaneously: visibility, atomicity, and mutual exclusion. Code guarded bysynchronizedexecutes on behalf of exactly one thread at a time by acquiring an intrinsic object monitor.
Comparative Overview
| Feature | volatile |
synchronized |
|---|---|---|
| Primary Guarantee | Visibility + Instruction Ordering | Visibility + Atomicity + Mutual Exclusion |
| Applicable Target | Variables (Fields only) | Methods and Code Blocks |
| Thread State | Non-blocking (Lock-Free) | Blocking (BLOCKED state on monitor contention) |
Atomicity of count++ |
❌ No (Prone to Race Conditions) | ✅ Yes (Critical section protected) |
| Deadlock Risk | ❌ Impossible | ⚠️ Possible |
| Performance Cost | Low (CPU memory fences & cache line invalidate) | Moderate to High (Lock inflation & context switches) |
Java 21 Code Examples
public class SynchronizedVsVolatileDemo {
// 1. volatile: Single-writer, multiple-reader lifecycle flag
public static class Worker implements Runnable {
private volatile boolean running = true;
public void stop() {
running = false; // Writes are immediately flushed across CPU core caches
}
@Override
public void run() {
while (running) {
// Background execution loop
}
}
}
// 2. synchronized: Atomic compound mutation
public static class Counter {
private int count = 0;
// Without synchronized, count++ loses increments under concurrent load
public synchronized void increment() {
count++; // Atomic execution: read -> modify -> write
}
public synchronized int get() {
return count;
}
}
}
🟡 Middle Level
Java Memory Model (JMM): Happens-Before and Memory Barriers
Under JSR-133 (Java Memory Model), modern multi-core processors maintain multi-level hardware caches (L1/L2/L3) and perform out-of-order execution.
1. The Happens-Before Invariant for volatile:
A write to a volatile field happens-before every subsequent read of that same volatile field.
Any state modifications made by Thread A to normal (non-volatile) variables prior to writing to a volatile field are guaranteed to be visible to Thread B after it reads that volatile field:
int payload = 0;
volatile boolean published = false;
// Thread 1:
payload = 42;
published = true; // StoreStore + StoreLoad barrier flushes 'payload' as well
// Thread 2:
if (published) {
System.out.println(payload); // Always prints 42; observing 0 is impossible
}
2. Hardware Memory Barriers (Fences):
The JIT compiler enforces JMM rules by emitting specific memory barrier instructions around volatile accesses:
- Prior to a
volatilewrite: Emits aStoreStorebarrier (flushes all preceding normal stores before the volatile write). - Following a
volatilewrite: Emits aStoreLoadbarrier (prevents subsequent reads/writes from reordering above this write). - Following a
volatileread: EmitsLoadLoadandLoadStorebarriers (prevents subsequent reads/writes from reordering above this read).
The Double-Checked Locking (DCL) Trap
A classic senior interview question is: “Why is volatile strictly mandatory on the instance variable in Double-Checked Locking?”
public class SafeDclSingleton {
// ⚠️ MUST BE volatile to prevent publication of partially initialized objects!
private static volatile SafeDclSingleton instance;
private SafeDclSingleton() {}
public static SafeDclSingleton getInstance() {
if (instance == null) { // 1st Check (Fast, lock-free)
synchronized (SafeDclSingleton.class) {
if (instance == null) { // 2nd Check (Guarded by monitor)
instance = new SafeDclSingleton();
}
}
}
return instance;
}
}
The Instruction Reordering Hazard
Instantiating an object (instance = new SafeDclSingleton()) compiles into three distinct steps:
allocate_memory()invoke_constructor()(Initialize field state)assign_reference(instance, pointer)
Without volatile, the JIT compiler and CPU may reorder step 3 before step 2 (1 -> 3 -> 2). If Thread B enters the first if (instance == null) check while Thread A is between step 3 and step 2, Thread B observes a non-null reference to a partially initialized object, reading corrupt or default field values. The volatile modifier establishes memory barriers that prevent step 3 from being published before step 2 finishes.
🔴 Senior Level
Hardware Assembly & MESI Cache Coherency
On x86 and x86-64 architectures:
- Volatile writes are compiled into instructions with the
lockprefix (e.g.,lock addl $0, 0(%rsp)orlock or). - The
lockprefix locks the specific 64-byte CPU cache line (Cache Lock). - Under the hardware MESI (Modified, Exclusive, Shared, Invalid) cache coherency protocol:
- The executing CPU core transitions its cache line containing the variable to the
Modifiedstate. - An Invalidate message is broadcast across the CPU interconnect bus.
- All other CPU cores mark their local L1/L2 cache lines holding that address as
Invalid, forcing them to fetch refreshed data from the L3 cache or main memory upon their next access.
- The executing CPU core transitions its cache line containing the variable to the
Bytecode Anatomy: Paired monitorenter and monitorexit
A synchronized block is compiled into explicit bytecode instructions:
aload_1
monitorenter // 1. Acquire object monitor
... critical section code ...
aload_1
monitorexit // 2. Normal execution monitor release
goto 14
astore_2
aload_1
monitorexit // 3. Synthetic exception handler monitor release!
aload_2
athrow
[!NOTE] The Java compiler (
javac) unconditionally generates twomonitorexitinstructions for everymonitorenter. The second is registered in a synthetic exception table handler covering the entire critical block, ensuring the monitor is released even if an unhandledThrowableorErroroccurs.
HotSpot Monitor Evolution
In the OpenJDK HotSpot JVM, synchronized is not implemented purely as an expensive OS-level mutex:
- Lightweight Locking (Thin Lock): The thread attempts to acquire the lock using an atomic CAS on the object’s
Mark Wordin its object header, saving aLock Recordon its execution stack. This completes in a few nanoseconds without kernel intervention. - Adaptive Spinning: If the lock is held, the thread spins in a loop (using CPU
PAUSEinstructions) anticipating that the lock will be released shortly. - Inflated Locking (Heavyweight
ObjectMonitor): Under sustained contention, the lock inflates into an OS-level mutex (pthread_mutex_t), parking contested threads in theBLOCKEDstate and incurring thread context-switch latency (1–5 µs).
False Sharing & @Contended (JEP 142)
When multiple volatile fields are placed in the same 64-byte cache line:
public class MetricCounters {
@jdk.internal.vm.annotation.Contended // Pads field with 128 bytes of padding
public volatile long writeOps;
public volatile long readOps;
}
Updating writeOps on Core 1 invalidates the entire cache line on Core 2, slowing down reads to readOps even though the variables are distinct. @Contended adds 128 bytes of memory padding, isolating high-throughput variables on dedicated cache lines.
4 Tricky Questions
1. Why is the operation volatile count++ not thread-safe under concurrent access?
Answer:
The increment operator count++ is not an atomic operation. At the bytecode level, it decomposes into three distinct operations:
getfield/aload: Read the current value ofcountfrom memory into a CPU register.iadd/iconst_1: Increment the value in the register by 1.putfield: Write the incremented value from the register back to memory.
Although volatile guarantees that reads and writes to memory are immediately visible, it does not prevent two threads from executing step 1 simultaneously. If both threads read 5, both will calculate 6 and write 6 back, resulting in a lost update race condition. For atomic compound mutations, use synchronized or AtomicInteger.
2. Why is the volatile modifier strictly mandatory on the singleton field in Double-Checked Locking (DCL)?
Answer:
Without volatile, the JVM and the CPU’s out-of-order execution engine are permitted to reorder the instructions involved in instance = new Singleton(). Specifically, memory allocation (1), field initialization (2), and reference assignment (3) can be reordered into 1 -> 3 -> 2.
If this occurs, another thread executing the first check if (instance == null) outside the synchronized block may observe a non-null reference while the constructor is still actively running in the first thread. The second thread would then receive an uninitialized or partially initialized object. Marking the field volatile inserts a StoreStore barrier that prevents reference assignment from being reordered ahead of constructor completion.
3. Which hardware memory barriers (fences) does the JIT compiler insert around volatile reads and writes?
Answer: To enforce JMM ordering:
- Before a
volatilewrite: AStoreStorebarrier is inserted to prevent previous normal stores from being reordered after the volatile write. - After a
volatilewrite: AStoreLoadbarrier is inserted to prevent the volatile write from being reordered with subsequent reads/writes. - After a
volatileread: ALoadLoadbarrier and aLoadStorebarrier are inserted to prevent subsequent reads and writes from being reordered before the volatile read.
4. How does javac guarantee that an acquired synchronized monitor is released if an unexpected runtime exception or error is thrown?
Answer:
The javac compiler generates an explicit synthetic exception table entry covering the bytecode instructions within the critical section.
In addition to the standard monitorexit placed immediately before the normal return instruction, the compiler generates a secondary exception handling path (any throwable catcher). This path catches any thrown Throwable, invokes monitorexit on the monitor reference stored in the local variable frame, and then re-throws the exception via athrow.
🎯 Interview Cheat Sheet
Comprehensive Comparison Matrix
| Dimension | volatile | synchronized | AtomicInteger |
| :— | :— | :— | :— |
| Applicability | Primitive/Reference Fields | Methods and Code Blocks | Standalone Wrapper Class |
| Compound Mutex (count++) | ❌ Race Condition | ✅ Atomic (via Monitor) | ✅ Atomic (via Hardware CAS) |
| Blocking Mechanism | Lock-Free | Blocking (BLOCKED status) | Lock-Free (Spin CAS loop) |
| Deadlock Vulnerability | ❌ None | ⚠️ Susceptible | ❌ None |
| Hardware Instruction | lock addl / mfence | monitorenter / monitorexit | lock cmpxchg |
Key Takeaways
volatile= Visibility + Ordering: Guarantees CPU cache coherence without thread suspension or mutual exclusion.synchronized= Mutual Exclusion + Atomicity: Enforces single-thread execution inside critical sections via object monitors.- Compound Failure:
volatilecannot make compound read-modify-write operations likei++atomic. - DCL Singleton:
volatileis strictly necessary in Double-Checked Locking to prevent exposure of partially initialized objects.
Red Flags & Anti-Patterns to Avoid
- ❌ Using
volatilefor counters: Results in lost updates under concurrent access. - ❌ Assuming
volatileturns off CPU caches: Caches remain active; consistency is enforced via MESI cache line invalidation. - ❌ Synchronizing on boxed primitives or String literals (
synchronized("lock")): Reused interned instances cause unexpected global lock contention. - ❌ Omitting
volatilein Double-Checked Locking: Exposes callers to half-initialized objects due to instruction reordering.