🚀AdvancedJVM Internals

Java Memory Model

Understand happens-before, visibility guarantees, volatile, and the Java Memory Model for correct concurrent programs.

Overview

The Java Memory Model (JMM) defines the rules for how threads interact through memory. Modern CPUs and compilers reorder instructions, cache values in registers, and optimize in ways that can make concurrent code behave unexpectedly unless you use the right synchronization primitives. The JMM introduces the "happens-before" relationship: if action A happens-before action B, then A's writes are visible to B. Without a happens-before relationship between two accesses to the same variable, the value read is undefined. The key mechanisms that establish happens-before are: synchronized blocks, volatile fields, thread start/join, and the Lock API. Understanding these is essential for writing correct multi-threaded code.

Key Points

  • •Happens-before (HB): if A HB B, all writes by A are visible to B
  • •synchronized: all writes in a synchronized block HB all subsequent reads synchronized on the same monitor
  • •volatile write HB volatile read of the same field (by any thread after the write)
  • •thread.start() HB all actions in the started thread
  • •All actions in a thread HB thread.join() in any other thread
  • •volatile prevents compiler and CPU reordering of accesses to that variable
  • •volatile is NOT a substitute for synchronization for compound operations like i++
  • •long and double are not atomically read/written without volatile (on 32-bit VMs)
  • •The "double-checked locking" pattern requires volatile on the instance field
  • •final fields in a correctly constructed object are visible to all threads without synchronization

Examples

Visibility bug without volatile

public class Main {
    static boolean running = true; // NOT volatile
    // static volatile boolean running = true; // correct

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            int count = 0;
            while (running) { // may never see running = false without volatile
                count++;
            }
            System.out.println("Stopped at count: " + count);
        });

        worker.start();
        Thread.sleep(100);
        running = false; // may not be visible to worker thread
        worker.join(2000);

        if (worker.isAlive()) {
            System.out.println("Worker still running! (visibility bug)");
            System.exit(0);
        }
    }
}

Without volatile, the JIT compiler may cache running in a register and the worker thread never sees the update. With volatile, every read goes to main memory and changes are immediately visible. Uncomment the volatile version to fix it.

Double-checked locking with volatile

public class ExpensiveResource {
    private static volatile ExpensiveResource instance; // volatile is required

    private ExpensiveResource() {
        System.out.println("Creating resource");
    }

    public static ExpensiveResource getInstance() {
        if (instance == null) {              // first check (no lock)
            synchronized (ExpensiveResource.class) {
                if (instance == null) {      // second check (with lock)
                    instance = new ExpensiveResource();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> ExpensiveResource.getInstance());
        Thread t2 = new Thread(() -> ExpensiveResource.getInstance());
        t1.start(); t2.start();
        t1.join(); t2.join();
        // "Creating resource" prints only once
    }
}

Without volatile, the JVM can reorder the write to instance. Thread 2 might see a non-null but partially constructed instance. volatile establishes a happens-before between the constructor write and the read of instance, preventing this.

final fields and safe publication

public class SafePoint {
    private final int x;
    private final int y;

    SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // Safe: any thread that can see the SafePoint reference
    // is guaranteed to see the final values of x and y
    @Override
    public String toString() { return "(" + x + ", " + y + ")"; }

    public static void main(String[] args) throws InterruptedException {
        SafePoint[] shared = new SafePoint[1];

        Thread writer = new Thread(() -> shared[0] = new SafePoint(3, 4));
        writer.start();
        writer.join();

        // After join, shared[0] is visible. Because x and y are final,
        // their values are also guaranteed visible.
        System.out.println(shared[0]);
    }
}

final fields are special in the JMM: once a constructor completes and the reference is published, all threads that see the reference are guaranteed to see the final field values written in the constructor, without synchronization.

Common Mistakes

  • Using volatile for compound operations like counter++. counter++ is read-modify-write: it reads the current value, adds 1, and writes back. Another thread can interleave between the read and write. Use AtomicInteger or synchronized for compound operations.
  • Assuming synchronized(this) in one method protects data from code synchronized(otherLock) in another method. Visibility is only guaranteed between threads using the SAME monitor object.
  • Publishing an object before its constructor finishes (this-escape). If a constructor registers this with an observer or starts a thread, other threads may see an incompletely initialized object.
  • Forgetting that even basic types like int are not safe to read/write from multiple threads without synchronization. Race conditions on int can produce values that were never written (tearing on 32-bit VMs for long/double).

Note

The Java Memory Model is formally defined in terms of happens-before edges in a partial order over actions. You do not need to memorize the formal rules; remember the practical summary: any synchronization primitive (synchronized, volatile, Lock, Atomic*, thread join/start) establishes happens-before. Without one, concurrent access to shared mutable state has undefined behavior.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What does "happens-before" mean in the Java Memory Model?