Skip to content
6 min read

Java Memory Model (JMM)

Why JMM is a FAANG Interview Favorite

The Java Memory Model defines how threads interact through memory and what behaviors are allowed in concurrent programs. Interviewers use JMM questions to test whether you truly understand concurrency or just memorize synchronized. If you cannot explain happens-before, visibility, and reordering — you will struggle with any senior-level concurrency question.


What is the Java Memory Model?

The JMM (defined in JSR-133, Java 5+) is a contract between the programmer, the JVM, and the hardware. It specifies:

  • When a write by one thread is guaranteed to be visible to a read by another thread
  • What reorderings are legal by the compiler, JIT, and CPU
  • What constitutes a data race and when code is correctly synchronized
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    subgraph "CPU Core 1"
        T1(("Thread-1"))
        L1_1[["L1 Cache"]]
        T1 --> L1_1
    end

    subgraph "CPU Core 2"
        T2(("Thread-2"))
        L1_2[["L1 Cache"]]
        T2 --> L1_2
    end

    subgraph "CPU Core 3"
        T3(("Thread-3"))
        L1_3[["L1 Cache"]]
        T3 --> L1_3
    end

    L2["Shared L2/L3 Cache"]
    MAIN(["Main Memory (Heap)"])

    L1_1 --> L2
    L1_2 --> L2
    L1_3 --> L2
    L2 --> MAIN

    style T1 fill:#DBEAFE,color:#1E40AF
    style T2 fill:#DBEAFE,color:#1E40AF
    style T3 fill:#DBEAFE,color:#1E40AF
    style L1_1 fill:#BFDBFE,color:#1E40AF
    style L1_2 fill:#BFDBFE,color:#1E40AF
    style L1_3 fill:#BFDBFE,color:#1E40AF
    style L2 fill:#FEF3C7,color:#1E40AF
    style MAIN fill:#D1FAE5,color:#1E40AF

Each thread may hold local copies of variables in CPU caches or registers. Without explicit synchronization, there is no guarantee that changes made by one thread will ever become visible to another.


Happens-Before Relationships

The happens-before (HB) relation is the core of the JMM. If action A happens-before action B, then A's effects are guaranteed visible to B.

All 8 Happens-Before Rules

# Rule Meaning
1 Program Order Within a single thread, each statement happens-before the next (in program order)
2 Monitor Lock An unlock on a monitor happens-before every subsequent lock on that same monitor
3 Volatile Variable A write to a volatile field happens-before every subsequent read of that field
4 Thread Start A call to thread.start() happens-before any action in the started thread
5 Thread Join All actions in a thread happen-before another thread returns from join() on that thread
6 Thread Interruption A call to thread.interrupt() happens-before the interrupted thread detects the interruption
7 Finalizer The end of a constructor happens-before the start of finalize() for that object
8 Transitivity If A happens-before B, and B happens-before C, then A happens-before C

Examples

Java
// Rule 2: Monitor Lock
synchronized (lock) {
    x = 42;              // Write inside critical section
}                        // unlock happens-before...
// ...another thread:
synchronized (lock) {    // ...this lock
    System.out.println(x); // Guaranteed to see 42
}

// Rule 4 & 5: Thread Start and Join
thread.start();          // Everything before start() is visible to the new thread
thread.join();           // Everything the thread did is now visible to the joining thread

Visibility Problems Without Synchronization

Without a happens-before relationship, one thread's writes may never be observed by another thread.

Java
// BROKEN CODE -- Thread may loop forever!
public class VisibilityBug {
    private boolean stop = false;  // not volatile

    public void writerThread() {
        stop = true;  // Written to CPU cache -- may never reach main memory
    }

    public void readerThread() {
        while (!stop) {
            // May NEVER terminate -- JIT can hoist the read out of the loop
            // Equivalent to: if (!stop) while(true) {}
        }
    }
}
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
sequenceDiagram
    participant T1 as Thread-1 (Writer)
    participant Cache1 as CPU Cache 1
    participant MM as Main Memory
    participant Cache2 as CPU Cache 2
    participant T2 as Thread-2 (Reader)

    T2->>Cache2: read stop (false)
    Note over T2: caches stop=false
    T1->>Cache1: write stop=true
    Note over Cache1: stays in local cache
    T2->>Cache2: read stop (still false!)
    T2->>Cache2: read stop (still false!)
    Note over T2: Loops forever...

Reordering

Compilers, JIT, and CPUs reorder instructions for performance. This is safe in single-threaded code but deadly in multithreaded code.

Types of Reordering

Level Who Does It Example
Compiler javac / JIT Reorder independent statements
Processor CPU out-of-order execution Execute loads before prior stores
Memory System Store buffers & cache coherence Writes become visible in different order

How Reordering Breaks Code

Java
// Thread-1
x = 42;        // (1)
ready = true;  // (2)

// Thread-2
if (ready) {           // (3)
    assert x == 42;    // (4) -- CAN FAIL!
}

Without synchronization, the CPU/compiler may reorder (1) and (2). Thread-2 could see ready == true but x == 0.

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
graph LR
    subgraph "Expected Order"
        A1["x = 42"] --> A2["ready = true"]
    end
    subgraph "Reordered by CPU"
        B1["ready = true"] --> B2["x = 42"]
    end

    style A1 fill:#D1FAE5,color:#1E40AF
    style A2 fill:#D1FAE5,color:#1E40AF
    style B1 fill:#FEE2E2,color:#1E40AF
    style B2 fill:#FEE2E2,color:#1E40AF

volatile Keyword Deep Dive

What volatile Guarantees

Guarantee Description
Visibility A write to a volatile variable is immediately flushed to main memory; a read always fetches from main memory
Ordering No reordering of volatile reads/writes with respect to surrounding code (acts as a memory fence)
NOT Atomicity volatile long count; count++ is NOT atomic (read-modify-write is 3 operations)

volatile Memory Semantics

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    VW1["Writes before"]:::before --> VW2["volatile write"]:::fence -.->|"happens-before"| VR1["volatile read"]:::fence --> VR2["Reads after"]:::after

    classDef before fill:#BFDBFE,stroke:#3B82F6,color:#1E40AF
    classDef fence fill:#FEF3C7,stroke:#F59E0B,color:#92400E
    classDef after fill:#BFDBFE,stroke:#3B82F6,color:#1E40AF

When volatile is Enough

Java
// CORRECT: single writer, multiple readers, simple flag
private volatile boolean shutdown = false;

// CORRECT: publishing an immutable object
private volatile Config config;

// BROKEN: volatile does NOT make this atomic
private volatile int counter = 0;
counter++;  // read → increment → write (3 steps, race condition!)

synchronized — Memory Semantics

synchronized provides mutual exclusion + memory visibility. Its memory semantics go beyond just locking.

Acquire and Release Semantics

Operation Memory Effect
Lock acquire (entering synchronized) Invalidate local cache — force reload from main memory
Lock release (exiting synchronized) Flush all writes to main memory before releasing
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
sequenceDiagram
    participant T1 as Thread-1
    participant Lock as Monitor Lock
    participant T2 as Thread-2

    T1->>Lock: acquire (enter synchronized)
    Note over T1: Reads from main memory (fresh)
    T1->>T1: x = 42, y = 100
    T1->>Lock: release (exit synchronized)
    Note over T1: Flush ALL writes to main memory

    T2->>Lock: acquire (enter synchronized)
    Note over T2: Invalidate caches, reload from main memory
    T2->>T2: read x (sees 42), read y (sees 100)
    T2->>Lock: release

Key insight: Synchronized flushes all writes, not just the variable being locked on. This is why locking on one monitor can make unrelated variables visible.


final Fields — Safe Publication Guarantee

The JMM guarantees that if an object is properly constructed (no this escaping the constructor), then all final fields are visible to any thread that obtains a reference to the object — without additional synchronization.

Java
public class ImmutableConfig {
    private final Map<String, String> settings;

    public ImmutableConfig(Map<String, String> input) {
        this.settings = Collections.unmodifiableMap(new HashMap<>(input));
        // After constructor completes, any thread seeing a reference to this object
        // is GUARANTEED to see the fully initialized 'settings' map
    }

    public String get(String key) {
        return settings.get(key);  // Safe without synchronization
    }
}

The this Escape Problem

If you leak this during construction (e.g., registering a listener), another thread may see partially constructed final fields.


Double-Checked Locking

The Broken Pattern (Pre-Java 5)

Java
// BROKEN -- DO NOT USE
public class BrokenSingleton {
    private static BrokenSingleton instance;

    public static BrokenSingleton getInstance() {
        if (instance == null) {           // (1) First check (no lock)
            synchronized (BrokenSingleton.class) {
                if (instance == null) {   // (2) Second check (with lock)
                    instance = new BrokenSingleton(); // (3) PROBLEM!
                }
            }
        }
        return instance;
    }
}

Why it breaks: Step (3) involves: allocate memory, invoke constructor, assign reference. The JVM can reorder so the reference is assigned before the constructor completes. Another thread at (1) sees a non-null but partially constructed object.

The Fix: volatile

Java
// CORRECT -- with volatile
public class Singleton {
    private static volatile Singleton instance;  // volatile prevents reordering

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                    // volatile write ensures the object is fully constructed
                    // before the reference becomes visible to other threads
                }
            }
        }
        return instance;
    }
}

Memory Barriers / Fences

A memory barrier (or fence) is a CPU instruction that enforces ordering constraints on memory operations.

Barrier Type Effect
LoadLoad All loads before the barrier complete before loads after it
StoreStore All stores before the barrier are flushed before stores after it
LoadStore All loads before the barrier complete before stores after it
StoreLoad All stores before the barrier are flushed before loads after it (most expensive)
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    subgraph "Without Barrier"
        direction LR
        NB1(["Store x = 1"]) -.->|"may reorder"| NB2(["Store y = 2"])
    end

    subgraph "With StoreStore Barrier"
        direction LR
        B1(["Store x = 1"]) --> FENCE["StoreStore Fence"] --> B2(["Store y = 2"])
    end

    style FENCE fill:#FEE2E2,color:#1E40AF,stroke-width:3px
    style B1 fill:#D1FAE5,color:#1E40AF
    style B2 fill:#D1FAE5,color:#1E40AF
    style NB1 fill:#BFDBFE,color:#1E40AF
    style NB2 fill:#BFDBFE,color:#1E40AF

In Java, you never insert barriers directly. They are emitted by the JVM when you use:

  • volatile reads/writes
  • synchronized enter/exit
  • java.util.concurrent.locks
  • VarHandle acquire/release fences (Java 9+)

Comparison: volatile vs synchronized vs Atomic

Feature volatile synchronized AtomicInteger / Atomics
Visibility Yes Yes Yes
Ordering (prevents reorder) Yes Yes Yes
Atomicity No (only single read/write) Yes (entire block) Yes (single operation: CAS)
Mutual exclusion No Yes No
Blocking No Yes (threads wait) No (lock-free, spins)
Performance Fastest Slowest (context switches) Middle (CAS loop)
Use case Flags, safe publication Critical sections, compound actions Counters, accumulators

Decision Flowchart

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    Q1["Compound action?"] -->|Yes| SYNC["synchronized"]:::sync
    Q1 -->|No| Q2["Atomic CAS?"] -->|Yes| ATOMIC["AtomicXxx"]:::atomic
    Q2 -->|No| Q3["Visibility only?"] -->|Yes| VOL["volatile"]:::vol
    Q3 -->|No| NONE["None needed"]:::none

    classDef sync fill:#FEE2E2,stroke:#EF4444,color:#991B1B
    classDef atomic fill:#FEF3C7,stroke:#F59E0B,color:#92400E
    classDef vol fill:#D1FAE5,stroke:#10B981,color:#065F46
    classDef none fill:#DBEAFE,stroke:#3B82F6,color:#1E40AF

Interview Questions

What is the happens-before relationship? Why does it matter?

The happens-before relationship is the JMM's guarantee that if action A happens-before action B, all memory effects of A are visible to B. Without it, threads have no guarantees about seeing each other's writes. It matters because it is the only way to reason about visibility in Java — if there is no happens-before between a write and a read, the read can return any value (stale, default, or current).

Can volatile replace synchronized? When is volatile insufficient?

No. volatile guarantees visibility and ordering but NOT atomicity. If you need compound actions (check-then-act, read-modify-write like count++), volatile is insufficient — you need synchronized, Lock, or AtomicXxx. Volatile is enough only when: (1) writes do not depend on the current value, or (2) only one thread ever writes.

Explain why double-checked locking is broken without volatile.

Object creation involves 3 steps: allocate memory, run constructor, assign reference. Without volatile, the JVM may reorder so the reference is assigned before the constructor finishes. Another thread sees a non-null reference and uses a partially constructed object. The volatile keyword prevents this reordering by inserting a StoreStore barrier before the reference assignment becomes visible.

What is the difference between visibility and atomicity?

Visibility means one thread's write is seen by another thread. Atomicity means an operation completes in one indivisible step. volatile gives visibility but not atomicity — volatile int x; x++ is still a race (read, increment, write are 3 separate steps). synchronized gives both. AtomicInteger.incrementAndGet() gives both through CAS.

If I write to a non-volatile variable inside a synchronized block, is it visible to other threads?

Yes — if and only if the reading thread also synchronizes on the same monitor. The lock release flushes all writes (not just the lock variable) to main memory, and the lock acquire invalidates the reader's cache. If the reader does not synchronize, there is no happens-before edge, and visibility is not guaranteed.

What are memory barriers and how does Java use them?

Memory barriers (fences) are CPU instructions that prevent reordering of loads and stores across the barrier. Java does not expose barriers directly to programmers. Instead, the JVM inserts them automatically when you use volatile, synchronized, or j.u.c constructs. For example, a volatile write inserts StoreStore + StoreLoad barriers, ensuring all prior writes are visible before the volatile write and that the volatile write is visible before any subsequent read.

Explain the publication problem with final fields and how the JMM solves it.

Without the final field guarantee, a thread could see a reference to a newly created object but see default values (0, null) in its fields — because the constructor writes might not yet be visible. The JMM solves this: for final fields, the end of the constructor happens-before any read of the final field via the object reference. This means if you get a reference to a properly constructed object, you are guaranteed to see the correct final field values — no synchronization needed.

Thread A writes x=1 then y=2. Thread B reads y==2 then reads x. Can Thread B see x==0?

Yes. Without synchronization, there is no happens-before relationship between Thread A's writes and Thread B's reads. The CPU/compiler may reorder A's writes (y=2 before x=1), or B's reads may see stale cached values. To guarantee B sees x==1 when it observes y==2, you must establish a happens-before edge — make y volatile, or protect both with the same lock.