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:
- Program Order: Within a thread, statements execute in order
- Monitor Lock: Unlock happens-before subsequent lock on same monitor
- volatile: Write to volatile happens-before read of that variable
- Thread Start:
thread.start()happens-before any action in that thread - Thread Join: All actions in thread happen-before
thread.join()returns - Transitivity: If A hb B, and B hb C, then A hb C
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:
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:
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:
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
volatileensure visibility? - What is the difference between
volatileandsynchronized? - Can
volatilemake 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
finalfields help with thread safety? - What is a data race?