Skip to content

Java Memory Model

The Java Memory Model (JMM) defines how threads interact through memory. Key concepts: happens-before relationship (guarantees visibility), volatile (ensures visibility, prevents reordering), synchronized (mutual exclusion + visibility). Without proper synchronization, threads may see stale values due to CPU caching and instruction reordering.


Memory Visibility Problem

Each thread may cache variables in CPU registers or CPU cache (L1/L2/L3), not reading from main memory. Without synchronization, one thread's write may never be visible to another thread. This is the fundamental problem the JMM solves through happens-before rules.

Deep Dive: Why Visibility Fails
// Thread 1
sharedVariable = 42;

// Thread 2
if (sharedVariable == 42) {  // May NEVER see the update!
    // ...
}

Why? Each thread may cache the variable in:

  • CPU registers (fastest, thread-local)
  • CPU cache (L1, L2, L3)
  • Main memory (slowest, shared)

Without synchronization, Thread 2 may read from its cache indefinitely.


Happens-Before Relationship

Happens-before is the JMM's core guarantee: if action A happens-before B, then A's results are visible to B. Key rules: program order (within a thread), monitor lock (unlock → lock), volatile (write → read), thread start (start() → thread's actions), thread join (thread's actions → join() return). Transitivity applies.

Deep Dive: Rules and Example

Rules:

  1. Program Order: Within a thread, statements execute in order
  2. Monitor Lock: Unlock happens-before subsequent lock on same monitor
  3. volatile: Write to volatile happens-before read of that variable
  4. Thread Start: thread.start() happens-before any action in that thread
  5. Thread Join: All actions in thread happen-before thread.join() returns
  6. Transitivity: If A hb B, and B hb C, then A hb C
// Thread 1
x = 1;              // (1)
synchronized (lock) {
    y = 2;          // (2)
}                   // (3) unlock

// Thread 2
synchronized (lock) {  // (4) lock — (3) hb (4)
    int a = y;         // (5) — guaranteed to see y = 2
}
int b = x;             // (6) — NO guarantee for x = 1!

volatile Semantics

volatile provides two guarantees: visibility (writes are immediately flushed to main memory, reads always fetch from main memory) and ordering (prevents instruction reordering around volatile accesses). Acts as a memory barrier — all writes before a volatile write are visible to any thread after a volatile read of that same variable.

Deep Dive: Memory Barrier Effect
class TaskRunner {
    private volatile boolean running = true;

    public void stop() {
        running = false;  // Flush to main memory + all prior writes
    }

    public void run() {
        while (running) {  // Reads from main memory
            // work
        }
    }
}

Memory barrier:

  • Write to volatile: Flushes ALL prior writes to main memory
  • Read from volatile: Invalidates cache, reads from main memory

volatile does NOT provide atomicity:

private volatile int count = 0;
count++;  // NOT atomic (read → add → write) — use AtomicInteger


synchronized Semantics

synchronized provides mutual exclusion (only one thread at a time) AND visibility (changes made inside the block are visible to the next thread that acquires the same lock). Entering a synchronized block invalidates the cache; exiting flushes all changes to main memory.

Deep Dive: Memory Barrier Effect
private int count = 0;
private final Object lock = new Object();

public void increment() {
    synchronized (lock) {
        // Enter: invalidate cache, read from main memory
        count++;
    }
    // Exit: flush changes to main memory
}

Both visibility AND atomicity — unlike volatile which only provides visibility.


Instruction Reordering

Compilers and CPUs reorder instructions for optimization as long as single-threaded behavior is preserved. But in multithreaded code, reordering can cause bugs — Thread B may see writes in a different order than Thread A intended. volatile and synchronized prevent harmful reordering.

Deep Dive: Reordering Bug Example
private int x = 0;
private boolean ready = false;

// Thread 1
public void write() {
    x = 42;           // (1)
    ready = true;     // (2) — CPU may reorder (1) and (2)!
}

// Thread 2
public void read() {
    if (ready) {       // Sees true
        int value = x; // But x might still be 0!
    }
}

Fix — make ready volatile:

private volatile boolean ready = false;
// Now (1) happens-before (2), and (2)→volatile write hb volatile read→(3)


Double-Checked Locking

Double-checked locking for Singleton is broken without volatile. Object construction isn't atomic — the JVM allocates memory, assigns the reference (non-null!), THEN calls the constructor. Without volatile, another thread can see the non-null reference before the constructor finishes, using a partially constructed object.

Deep Dive: Broken vs Fixed

Broken (without volatile):

private static Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {                  // (1) Check
        synchronized (Singleton.class) {
            if (instance == null) {           // (2) Double-check
                instance = new Singleton();   // (3) NOT atomic!
            }
        }
    }
    return instance;
}

Problem at step (3):

1. Allocate memory
2. Assign reference to instance (now != null)
3. Call constructor ← Thread B may use instance before this!

Fixed:

private static volatile Singleton instance;
// volatile prevents reordering — constructor finishes before assignment


Safe Publication

To safely share an object across threads, use one of: static initializer (class loading guarantees), volatile field, AtomicReference, final field (for immutable objects), or proper synchronization. Publishing without these mechanisms may expose partially constructed objects.

Deep Dive: Safe Publication Methods
// 1. Static initializer
public static final MyObject INSTANCE = new MyObject();

// 2. Volatile field
private volatile MyObject object;

// 3. AtomicReference
private final AtomicReference<MyObject> ref = new AtomicReference<>();

// 4. Final field (immutable objects)
private final MyObject object;  // All threads see correct final fields

// 5. Synchronized
synchronized (lock) { object = new MyObject(); }
Deep Dive: Improper Construction (Reference Escape)
public class Unsafe {
    public static Unsafe instance;
    private final int value;

    public Unsafe(int value) {
        this.value = value;
        Unsafe.instance = this;  // ❌ Reference escapes during construction!
        // Other threads may see partially constructed object
    }
}

Rule: Never let this escape during construction (don't start threads, register listeners, or assign to static fields from the constructor).


Common Interview Questions

Common Interview Questions
  • What is the Java Memory Model?
  • What is the happens-before relationship?
  • How does volatile ensure visibility?
  • What is the difference between volatile and synchronized?
  • Can volatile make compound operations atomic?
  • What is instruction reordering? When does it happen?
  • Explain the double-checked locking problem and its fix.
  • What are memory barriers?
  • What is safe publication?
  • How do final fields help with thread safety?
  • What is a data race?