Skip to content
Home / Skills / Java / Memory Model
JA

Memory Model

Java advanced v1.0.0

Java Memory Model

Overview

The Java Memory Model (JMM) defines how threads interact through memory and what behaviors are allowed in concurrent programs. Understanding the JMM is essential for writing correct concurrent code and avoiding subtle bugs related to visibility and ordering.


Key Concepts

Memory Architecture

┌─────────────────────────────────────────────────────────────┐
│                  Java Memory Architecture                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Thread 1           Thread 2           Thread 3            │
│   ┌───────┐         ┌───────┐          ┌───────┐           │
│   │ Local │         │ Local │          │ Local │           │
│   │ Cache │         │ Cache │          │ Cache │           │
│   └───┬───┘         └───┬───┘          └───┬───┘           │
│       │                 │                   │               │
│       │    Flush/       │    Flush/        │               │
│       │    Refresh      │    Refresh       │               │
│       │                 │                   │               │
│   ────┴─────────────────┴───────────────────┴────           │
│                         │                                    │
│                    ┌────┴────┐                              │
│                    │  Main   │                              │
│                    │ Memory  │                              │
│                    └─────────┘                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Happens-Before Relationship

The happens-before relationship guarantees that memory writes by one statement are visible to another.

Key happens-before rules:

  1. Program Order Rule: Each action in a thread happens-before every subsequent action in that thread
  2. Monitor Lock Rule: Unlock happens-before subsequent lock of same monitor
  3. Volatile Variable Rule: Write to volatile happens-before subsequent read
  4. Thread Start Rule: Thread.start() happens-before any action in started thread
  5. Thread Termination Rule: Any action in thread happens-before Thread.join()
  6. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C

Best Practices

1. Use Volatile for Simple Flags

For simple read-write flags shared between threads, volatile is sufficient.

2. Prefer Atomic Classes for Counters

Use AtomicInteger, AtomicLong, etc. for thread-safe counters.

3. Use Final Fields for Immutability

Final fields guarantee visibility after construction.

4. Avoid Publishing ‘this’ in Constructors

The object may be seen in a partially constructed state.

5. Synchronize Access to All Mutable Shared State

Both reads and writes need synchronization.


Code Examples

Example 1: Volatile Visibility

public class VolatileExample {
    
    // Without volatile - may never see update
    private boolean stopRequested = false;
    
    // With volatile - guaranteed visibility
    private volatile boolean stopRequestedSafe = false;
    
    // BROKEN - thread may never stop
    public void brokenExample() {
        new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                i++; // JIT may hoist the check out of loop
            }
        }).start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        stopRequested = true; // May never be seen by other thread
    }
    
    // CORRECT - with volatile
    public void correctExample() {
        new Thread(() -> {
            int i = 0;
            while (!stopRequestedSafe) {
                i++; // Will check volatile each iteration
            }
        }).start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        stopRequestedSafe = true; // Guaranteed to be visible
    }
    
    // Volatile is NOT atomic for compound operations
    private volatile int counter = 0;
    
    public void brokenIncrement() {
        counter++; // NOT atomic! Read-modify-write
    }
    
    // Use AtomicInteger instead
    private final AtomicInteger atomicCounter = new AtomicInteger(0);
    
    public void correctIncrement() {
        atomicCounter.incrementAndGet(); // Atomic operation
    }
}

Example 2: Safe Publication

public class SafePublication {
    
    // UNSAFE - other threads may see partially constructed Holder
    public Holder unsafeHolder;
    
    // SAFE - volatile guarantees visibility of fully constructed object
    public volatile Holder volatileHolder;
    
    // SAFE - final guarantees visibility after construction
    public final Holder finalHolder;
    
    // SAFE - synchronized access
    private Holder synchronizedHolder;
    
    public SafePublication() {
        this.finalHolder = new Holder(42);
    }
    
    public synchronized Holder getSynchronizedHolder() {
        return synchronizedHolder;
    }
    
    public synchronized void setSynchronizedHolder(Holder holder) {
        this.synchronizedHolder = holder;
    }
    
    // Immutable objects are always thread-safe to publish
    public record ImmutableHolder(int value, String name, List<String> items) {
        public ImmutableHolder {
            // Defensive copy for safe publication
            items = List.copyOf(items);
        }
    }
    
    // Safe initialization holder idiom
    private static class LazyHolder {
        static final ExpensiveObject INSTANCE = new ExpensiveObject();
    }
    
    public static ExpensiveObject getInstance() {
        return LazyHolder.INSTANCE; // Thread-safe lazy initialization
    }
}

class Holder {
    private final int value;
    
    public Holder(int value) {
        this.value = value;
    }
    
    public void assertSanity() {
        if (value != value) { // Can fail without safe publication!
            throw new AssertionError("Sanity check failed");
        }
    }
}

Example 3: Memory Barriers and Ordering

public class MemoryBarriers {
    
    private int a = 0;
    private int b = 0;
    private volatile boolean flag = false;
    
    // Without proper synchronization, reordering is possible
    public void writer() {
        a = 1;          // (1)
        b = 2;          // (2)
        flag = true;    // (3) - Volatile write is a release barrier
        
        // Volatile write prevents reordering of (1), (2) after (3)
        // Reader is guaranteed to see a=1, b=2 after seeing flag=true
    }
    
    public void reader() {
        if (flag) {     // Volatile read is an acquire barrier
            // Guaranteed to see a=1, b=2
            assert a == 1;
            assert b == 2;
        }
    }
    
    // Using VarHandle for explicit memory ordering (Java 9+)
    private static final VarHandle FLAG_HANDLE;
    private boolean flagWithVarHandle = false;
    
    static {
        try {
            FLAG_HANDLE = MethodHandles.lookup()
                .findVarHandle(MemoryBarriers.class, "flagWithVarHandle", boolean.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    
    public void writerWithVarHandle() {
        a = 1;
        b = 2;
        // Release semantics - all prior writes visible
        FLAG_HANDLE.setRelease(this, true);
    }
    
    public void readerWithVarHandle() {
        // Acquire semantics - subsequent reads see prior writes
        if ((boolean) FLAG_HANDLE.getAcquire(this)) {
            assert a == 1;
            assert b == 2;
        }
    }
    
    // Opaque access - no reordering, but no synchronization
    public void opaqueAccess() {
        FLAG_HANDLE.setOpaque(this, true);  // No reordering
        boolean val = (boolean) FLAG_HANDLE.getOpaque(this);
    }
}

Example 4: Final Fields Semantics

public class FinalFieldsSemantics {
    
    // Final fields have special publication guarantees
    public class SafelyPublished {
        private final int x;
        private final Object ref;
        private final int[] array;
        
        public SafelyPublished(int x, Object ref, int[] array) {
            this.x = x;
            this.ref = ref;
            // Make defensive copy for arrays
            this.array = Arrays.copyOf(array, array.length);
        }
        
        // After construction, other threads are guaranteed to see
        // correctly initialized final fields (even without synchronization)
    }
    
    // UNSAFE - 'this' escapes before construction completes
    public class UnsafeEscape {
        private final int value;
        
        public UnsafeEscape(EventSource source) {
            source.registerListener(
                e -> doSomething(value)  // 'this' escapes!
            );
            value = 42;  // May not be visible when listener is called
        }
    }
    
    // SAFE - don't let 'this' escape
    public class SafeConstruction {
        private final int value;
        private final EventListener listener;
        
        private SafeConstruction(int value) {
            this.value = value;
            this.listener = e -> doSomething(this.value);
        }
        
        // Factory method registers listener after construction
        public static SafeConstruction create(int value, EventSource source) {
            SafeConstruction instance = new SafeConstruction(value);
            source.registerListener(instance.listener);
            return instance;
        }
    }
    
    // Final field freeze extends to objects reachable from final field
    public class DeepFreeze {
        private final Map<String, Config> configs;
        
        public DeepFreeze(Map<String, Config> configs) {
            // Defensive copy
            this.configs = Map.copyOf(configs);
        }
        
        // Thread reading this object after publication will see
        // fully initialized Map AND all Config objects within it
    }
}

Example 5: Double-Checked Locking Done Right

public class DoubleCheckedLocking {
    
    // BROKEN without volatile
    private static Object unsafeInstance;
    
    public static Object getUnsafeInstance() {
        if (unsafeInstance == null) {
            synchronized (DoubleCheckedLocking.class) {
                if (unsafeInstance == null) {
                    unsafeInstance = new Object(); // May see partially constructed
                }
            }
        }
        return unsafeInstance; // May return partially constructed object!
    }
    
    // CORRECT with volatile
    private static volatile Object safeInstance;
    
    public static Object getSafeInstance() {
        Object result = safeInstance;
        if (result == null) {
            synchronized (DoubleCheckedLocking.class) {
                result = safeInstance;
                if (result == null) {
                    safeInstance = result = new Object();
                }
            }
        }
        return result;
    }
    
    // BETTER - Initialization-on-demand holder idiom
    private static class InstanceHolder {
        static final ExpensiveObject INSTANCE = new ExpensiveObject();
    }
    
    public static ExpensiveObject getInstance() {
        return InstanceHolder.INSTANCE;
    }
    
    // BEST for Java 9+ - use VarHandle
    private static Object varHandleInstance;
    private static final VarHandle INSTANCE_HANDLE;
    
    static {
        try {
            INSTANCE_HANDLE = MethodHandles.lookup()
                .findStaticVarHandle(DoubleCheckedLocking.class, "varHandleInstance", Object.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    
    public static Object getInstanceWithVarHandle() {
        Object result = (Object) INSTANCE_HANDLE.getAcquire();
        if (result == null) {
            synchronized (DoubleCheckedLocking.class) {
                result = (Object) INSTANCE_HANDLE.getAcquire();
                if (result == null) {
                    result = new Object();
                    INSTANCE_HANDLE.setRelease(result);
                }
            }
        }
        return result;
    }
}

Anti-Patterns

❌ Assuming Visibility Without Synchronization

// WRONG - no visibility guarantee
private boolean done = false;
private int result;

public void compute() {
    result = heavyComputation();
    done = true;  // No guarantee other threads see this
}

public int getResult() {
    while (!done); // May spin forever
    return result; // May see stale value
}

❌ Non-Atomic Read-Modify-Write on Volatile

// WRONG - not atomic
private volatile int counter = 0;

public void increment() {
    counter++; // Three operations: read, increment, write
}

❌ Incorrect Lazy Initialization

// WRONG - race condition
private Object instance;

public Object getInstance() {
    if (instance == null) {
        instance = new Object(); // Multiple threads may create
    }
    return instance;
}

Testing Strategies

Testing Memory Visibility

@Test
void shouldEnsureVisibilityWithVolatile() throws Exception {
    AtomicBoolean sawUpdate = new AtomicBoolean(false);
    VolatileFlag flag = new VolatileFlag();
    
    Thread reader = new Thread(() -> {
        while (!flag.isSet()) {
            // Spin
        }
        sawUpdate.set(true);
    });
    
    reader.start();
    Thread.sleep(100); // Give reader time to start
    flag.set();
    reader.join(1000);
    
    assertThat(sawUpdate.get()).isTrue();
    assertThat(reader.isAlive()).isFalse();
}

// JCStress for exhaustive concurrency testing
@JCStressTest
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Both see update")
@Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "Neither sees update")
@State
public class VolatileVisibilityTest {
    volatile int x;
    int y;
    
    @Actor
    public void writer() {
        y = 1;
        x = 1;
    }
    
    @Actor
    public void reader(II_Result r) {
        r.r1 = x;
        r.r2 = y;
    }
}

References