🧵 Section 9 · Question #1

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:

  • volatile solves the visibility problem and prevents CPU instruction reordering. Writes to a volatile variable are immediately flushed across CPU caches, making the update instantly visible to all other threads. However, volatile does not provide atomicity for compound read-modify-write operations (such as count++).
  • synchronized solves three problems simultaneously: visibility, atomicity, and mutual exclusion. Code guarded by synchronized executes 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 volatile write: Emits a StoreStore barrier (flushes all preceding normal stores before the volatile write).
  • Following a volatile write: Emits a StoreLoad barrier (prevents subsequent reads/writes from reordering above this write).
  • Following a volatile read: Emits LoadLoad and LoadStore barriers (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:

  1. allocate_memory()
  2. invoke_constructor() (Initialize field state)
  3. 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 lock prefix (e.g., lock addl $0, 0(%rsp) or lock or).
  • The lock prefix locks the specific 64-byte CPU cache line (Cache Lock).
  • Under the hardware MESI (Modified, Exclusive, Shared, Invalid) cache coherency protocol:
    1. The executing CPU core transitions its cache line containing the variable to the Modified state.
    2. An Invalidate message is broadcast across the CPU interconnect bus.
    3. 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.

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 two monitorexit instructions for every monitorenter. The second is registered in a synthetic exception table handler covering the entire critical block, ensuring the monitor is released even if an unhandled Throwable or Error occurs.


HotSpot Monitor Evolution

In the OpenJDK HotSpot JVM, synchronized is not implemented purely as an expensive OS-level mutex:

  1. Lightweight Locking (Thin Lock): The thread attempts to acquire the lock using an atomic CAS on the object’s Mark Word in its object header, saving a Lock Record on its execution stack. This completes in a few nanoseconds without kernel intervention.
  2. Adaptive Spinning: If the lock is held, the thread spins in a loop (using CPU PAUSE instructions) anticipating that the lock will be released shortly.
  3. Inflated Locking (Heavyweight ObjectMonitor): Under sustained contention, the lock inflates into an OS-level mutex (pthread_mutex_t), parking contested threads in the BLOCKED state 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:

  1. getfield / aload: Read the current value of count from memory into a CPU register.
  2. iadd / iconst_1: Increment the value in the register by 1.
  3. 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 volatile write: A StoreStore barrier is inserted to prevent previous normal stores from being reordered after the volatile write.
  • After a volatile write: A StoreLoad barrier is inserted to prevent the volatile write from being reordered with subsequent reads/writes.
  • After a volatile read: A LoadLoad barrier and a LoadStore barrier 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: volatile cannot make compound read-modify-write operations like i++ atomic.
  • DCL Singleton: volatile is strictly necessary in Double-Checked Locking to prevent exposure of partially initialized objects.

Red Flags & Anti-Patterns to Avoid

  • ❌ Using volatile for counters: Results in lost updates under concurrent access.
  • ❌ Assuming volatile turns 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 volatile in Double-Checked Locking: Exposes callers to half-initialized objects due to instruction reordering.