Java Memory Model: Happens-Before, Volatile, and Final Fields

Understanding happens-before guarantees, volatile field semantics, and final field safety in the Java Memory Model for correct concurrent code.

published: reading time: 23 min read author: GeekWorkBench

Java Memory Model: Happens-Before, Synchronized, Volatile, and Final Field Semantics

The Java Memory Model sounds like academic fluff until you’re debugging a bug that only appears in production at 3am. The JMM is the set of rules that govern how the JVM handles memory visibility and operation ordering across threads. Get these wrong and you’ll spend weeks chasing data races that vanish when you add logging.

If you’re new to threads, start with Threads and Lightweight Processes for foundational concepts.

Introduction

The Java Memory Model (JMM) is the set of rules that govern how the JVM handles memory visibility and operation ordering across threads. It defines the guarantees that Java programs can rely on when multiple threads access shared memory simultaneously. Without these rules, you would have no reliable way to reason about what one thread sees when another thread writes to a field—the JVM and CPU could reorder operations in ways that produce bugs invisible during testing and catastrophic in production.

The practical consequence of ignoring the JMM is data races: situations where threads disagree on the state of shared data because visibility guarantees are violated. These bugs are notoriously difficult to debug because they often disappear when you add logging (logging introduces synchronization that masks the race), they may only appear under specific CPU architectures or JVM configurations, and they typically resist attempts to reproduce them consistently. The JMM gives you the vocabulary and the tools to identify and fix these issues systematically rather than by trial and error.

This post covers the happens-before relationship that forms the foundation of JMM guarantees, explains how synchronized and volatile establish ordering constraints, and clarifies why final fields have special safety semantics. By the end, you will be able to analyze concurrent code for data races, apply the correct synchronization primitive for each situation, and understand why patterns like double-checked locking require volatile to work correctly. You will also have a clear mental model for reasoning about thread visibility in any Java program you encounter.

What Is the Java Memory Model?

The JMM defines the behavior of memory operations in concurrent Java programs. It specifies how reads and writes to memory happen across threads, ensuring that certain operations appear to happen in a specific order relative to each other.

The core problem the JMM solves is this: without proper synchronization, a thread might see stale data written by another thread, or observe operations in an order that differs from program order. The JMM provides a formal specification for when these visibility guarantees hold.

Modern processors and compilers perform optimizations that can reorder memory operations for performance. The JMM defines a set of rules called happens-before relationships that establish when these reorderings are prohibited.

Happens-Before Relationships

If one action happens-before another, the first action’s results are guaranteed to be visible to the second action. That’s the definition, but let’s look at where these guarantees come from.

Key Happens-Before Rules

Here are the rules that actually matter in practice:

  1. Program Order Rule: Within each thread, each action happens-before any subsequent action in that same thread
  2. Monitor Lock Rule: An unlock of a monitor happens-before each subsequent lock on that same monitor
  3. Volatile Field Rule: A write to a volatile field happens-before each subsequent read of that same field
  4. Thread Start Rule: A call to Thread.start() happens-before any action in the started thread
  5. Thread Termination Rule: A thread termination action happens-before any action that detects the thread has terminated
  6. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C

Practical Example

Here’s a classic data race that trips up a lot of developers:

public class DataRaceExample {
    private int counter = 0;
    private boolean ready = false;

    public void writer() {
        counter = 42;      // Action A
        ready = true;       // Action B
    }

    public void reader() {
        if (ready) {        // Action C
            System.out.println(counter); // Action D
        }
    }
}

Without synchronization, the reader might see counter = 0 even when ready = true. The JMM doesn’t guarantee visibility across threads without happens-before relationships. Making ready volatile fixes this:

private volatile boolean ready = false;

Now the write to ready happens-before the read of ready, ensuring the write to counter is also visible (due to transitivity with the Program Order Rule).

Synchronized Blocks

The synchronized keyword gives you mutual exclusion plus happens-before guarantees. When a thread acquires a monitor and then releases it, all memory operations before the release become visible to the next thread that acquires the same monitor.

Synchronized Method vs Block

public class SynchronizedExample {
    private final Object lock = new Object();
    private int value = 0;

    // Instance method synchronization - uses 'this' as monitor
    public synchronized void increment() {
        value++;
    }

    // Equivalent block syntax
    public void incrementBlock() {
        synchronized (this) {
            value++;
        }
    }

    // Separate lock object for finer control
    public void processWithSeparateLock() {
        synchronized (lock) {
            // Critical section with dedicated lock
            performOperation();
        }
    }
}

Happens-Before Guarantees with Synchronized

When you exit a synchronized block:

  1. All memory operations within the block become visible to the next thread that enters a synchronized block on the same monitor
  2. The monitor unlock action happens-before any subsequent lock action on that same monitor
  3. This applies even if the threads are running on different cores or processors

For more on lock-related issues, see Deadlock and Starvation and Locking and Concurrency.

Common Pitfalls

  • Over-synchronization: Holding locks for too long degrades performance and can cause deadlocks
  • Inconsistent lock ordering: Acquiring multiple locks in different orders across threads causes deadlock
  • Holding locks during blocking operations: Never do I/O or wait() while holding a lock

Volatile Fields

Volatile gives you weaker guarantees than synchronized but with less overhead. A write to a volatile field is visible to all subsequent reads of that same field.

When to Use Volatile

Volatile works when:

  • You need visibility guarantees for a single variable across threads
  • The variable is accessed by multiple threads without compound operations
  • You want to signal state changes between threads
public class VolatileExample {
    // Use volatile for flags and state variables
    private volatile boolean shutdownRequested = false;
    private volatile long lastUpdateTime = 0;

    public void requestShutdown() {
        shutdownRequested = true;
    }

    public void doWork() {
        while (!shutdownRequested) {
            // Work loop
            lastUpdateTime = System.nanoTime();
        }
    }
}

When NOT to Use Volatile

Volatile is insufficient when:

  • You need atomic operations on multiple variables
  • You need mutual exclusion
  • Compound operations are required (e.g., counter++)
// BROKEN - counter++ is not atomic even with volatile
private volatile int counter = 0;

// This still has a data race!
public void increment() {
    counter++; // Read-modify-write is not atomic
}

// CORRECT - use AtomicInteger for compound operations
// See also: Lock-Free Data Structures
private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet();
}

Volatile andhappens-before

A write to a volatile field has happens-before semantics with all subsequent reads of that field. This means:

  • Writes to non-volatile variables that happen-before a volatile write are visible after a volatile read
  • This is called the “volatile variable rule” and is a key part of double-checked locking

Final Field Semantics

Final fields get special treatment in the JMM. The JVM guarantees that once a constructor finishes, all threads see the correctly initialized values of final fields.

Why Final Fields Are Special

Without special handling, a partially constructed object could be visible to another thread if a reference to the object escapes before the constructor completes.

public class FinalFieldExample {
    private final int value;
    private final String name;
    private final List<String> items;

    public FinalFieldExample(int value, String name) {
        this.value = value;
        this.name = name;
        this.items = new ArrayList<>();
        items.add("initialized");
    }

    // The reference 'this' should NEVER escape during construction
    // Bad example - DON'T do this:
    // public static FinalFieldExample instance;
    // public FinalFieldExample(int v) {
    //     instance = this; // ESCAPE - 'this' escapes!
    // }
}

Safe Publication Patterns

For safe publication of objects with final fields:

  1. Use final fields: The JVM provides special guarantees
  2. Use static initializer: Static fields are initialized during class loading
  3. Use final reference + final fields: Combining both provides strongest guarantees
  4. Use synchronized or volatile: These establish happens-before guarantees
// Safe publication using static holder pattern
public class SafePublication {
    private final int value;
    private final String description;

    public SafePublication(int value, String description) {
        this.value = value;
        this.description = description;
    }

    // Safe publication via static initializer
    private static final SafePublication INSTANCE = new SafePublication(42, "Meaning of life");

    public static SafePublication getInstance() {
        return INSTANCE;
    }
}

Architecture Diagram

Here’s how the happens-before relationships fit together:

Synchronized Block Happens-Before

graph TB
    subgraph Thread1
        A1[Write to Variable] --> A2[Unlock Monitor]
    end

    subgraph Thread2
        B1[Lock Monitor] --> B2[Read Variable]
    end

    A2 -.->|happens-before| B1

Volatile Field Happens-Before

graph TB
    subgraph VolatileExample
        C1[Write to Volatile] --> C2[Read Volatile]
    end

    C1 -.->|happens-before| C2

Final Fields Safe Publication

graph TB
    subgraph FinalFields
        D1[Constructor Completes] --> D2[Reference Published]
    end

    D1 -.->|safe publication| D2

Production Failure Scenarios

Scenario 1: Double-Checked Locking with Non-Volatile Reference

// BROKEN - classic double-checked locking failure
public class BrokenSingleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                 // First check
            synchronized (BrokenSingleton.class) {
                if (instance == null) {         // Second check
                    instance = new Singleton(); // Problem here!
                }
            }
        }
        return instance;
    }
}

Failure: The instance = new Singleton() operation involves multiple steps (allocate memory, call constructor, assign reference). Without volatile, another thread might see a non-null instance but with uninitialized fields.

Fix: Make instance volatile:

private static volatile Singleton instance;

Scenario 2: Publication via Constructor Argument

// BROKEN - unsafe publication
public class UnsafePublisher {
    private final int value;

    public UnsafePublisher(Consumer<Integer> consumer) {
        this.value = computeValue();
        consumer.accept(this.value); // 'this' escapes!
    }
}

Failure: The this reference escapes during construction, allowing another thread to observe the object in a partially constructed state.

Fix: Never pass this to another object during construction.

Scenario 3: Volatile for State Machine Without Atomic Transitions

// BROKEN - state transitions are not atomic
private volatile int state = 0;

public void transition() {
    if (state == 0) {
        // Another thread might also enter this block
        state = 1; // Race condition
        performTransition();
    }
}

Fix: Use synchronized or AtomicInteger for compound operations.

Trade-off Table

ApproachVisibilityAtomicityPerformanceUse When
No synchronizationNone guaranteedN/ABestSingle-threaded only
synchronizedAll memory opsMutual exclusionModerate overheadMutual exclusion needed
volatileAll memory opsSingle variableLow overheadFlag/state signals
Atomic* classesAll memory opsSingle operationLow overheadCounter, flags, references
final fieldsSafe constructionImmutabilityNo overheadConstants, immutable data

Implementation Snippets

Safe Counter with Multiple Fields

public class SafeCounter {
    private final Object lock = new Object();
    private long count = 0;
    private long lastUpdate = 0;

    public void increment() {
        synchronized (lock) {
            count++;
            lastUpdate = System.nanoTime();
        }
    }

    public long getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Flag-Based Shutdown with Volatile

public class VolatileShutdownFlag {
    private volatile boolean shuttingDown = false;

    public void initiateShutdown() {
        shuttingDown = true;
    }

    public boolean isShuttingDown() {
        return shuttingDown;
    }

    // Use in work loop
    public void runLoop() {
        while (!shuttingDown) {
            processNextTask();
        }
        cleanup();
    }
}

Safe Listener Registration

public class SafeListenerRegistration {
    private final List<Listener> listeners = new ArrayList<>();

    // Safe - no 'this' escape
    public void registerListener(Listener listener) {
        synchronized (listeners) {
            listeners.add(listener);
        }
    }

    // Safe publication via final field + synchronized access
    private final List<Listener> listenersCopy;

    public SafeListenerRegistration() {
        List<Listener> temp = new ArrayList<>();
        // populate temp
        this.listenersCopy = Collections.unmodifiableList(temp);
    }

    public List<Listener> getListeners() {
        return listenersCopy;
    }
}

Observability Checklist

When debugging concurrency issues related to the JMM:

  • Is the field volatile if it accessed across threads without synchronization?
  • Are all shared mutable fields properly synchronized or volatile?
  • Does any code hold locks for longer than necessary?
  • Is there a potential for lock ordering deadlocks?
  • Are final fields being properly initialized before publication?
  • Is ‘this’ reference escaping during construction?
  • Are atomic operations truly independent (no read-modify-write races)?
  • Can you verify happens-before relationships in your code paths?

Security Notes

Concurrency bugs can become security vulnerabilities:

  • Time-of-check to time-of-use (TOCTOU): Checking a condition and acting on it is not atomic
  • Uninitialized data exposure: Final field semantics prevent this if used correctly
  • Order confusion attacks: Without happens-before guarantees, an attacker might observe incorrect operation ordering
  • Side-channel timing attacks: Lock contention timing can leak information about sensitive operations

Always use secure coding practices combined with proper synchronization.

Common Pitfalls / Anti-Patterns

  1. Assuming visibility without synchronization: A write in one thread may never be visible to another without happens-before guarantees
  2. Using volatile for compound operations: volatile does not make i++ atomic
  3. Nested locks on different objects: Can cause deadlocks if lock ordering differs across threads
  4. Returning mutable objects from getters: Always return copies or immutable views
  5. Starting threads in constructors: This escapes this before the object is fully constructed
  6. Ignoring exception handling in concurrent code: Exceptions can leave locks in unexpected states

Quick Recap Checklist

  • Happens-before guarantees ensure memory visibility across threads
  • Synchronized provides mutual exclusion and happens-before for all memory operations
  • Volatile ensures writes are visible to subsequent reads of the same field
  • Final fields provide safety guarantees when properly constructed
  • Never let this escape during construction
  • Use atomic classes for compound operations on counters and flags
  • Double-checked locking requires volatile on the reference
  • Transitivity extends happens-before across thread boundaries

Interview Questions

1. What is the difference between synchronized and volatile in Java?

Synchronized provides both mutual exclusion and happens-before guarantees for all memory operations within the synchronized block. It ensures that only one thread executes the critical section at a time and that all memory changes are visible when entering and exiting the synchronized block.

Volatile provides weaker guarantees - only ensuring that writes to the volatile field are visible to subsequent reads of the same field. It does not provide atomicity for compound operations. Volatile has lower overhead than synchronized and never causes thread blocking.

2. Explain the happens-before relationship and why it matters in concurrent Java code.

A happens-before relationship guarantees that if action A happens-before action B, then the results of A are visible to B. This is critical because modern JVMs and CPUs can reorder memory operations for performance. Without happens-before guarantees, a thread might see stale data or operations in an unexpected order.

The JMM defines specific rules: program order within a thread, monitor lock/unlock ordering, volatile read/write ordering, thread start/join ordering, and transitivity. These rules allow developers to reason about correctness in concurrent programs.

3. Why is double-checked locking broken without volatile?

Double-checked locking attempts to reduce synchronization overhead for lazy initialization. However, without volatile on the instance field, the JVM can reorder the assignment of the reference with the execution of the constructor. A thread might see a non-null reference while the object's fields are still at their default values (zero, null, false).

Making the instance field volatile prevents this reordering by establishing a happens-before relationship between the write to the field and subsequent reads, ensuring the constructor has fully completed before the reference becomes visible.

4. How do final fields provide safety guarantees in concurrent Java programs?

Final fields have special semantics that prevent observation of partially constructed objects. The JVM ensures that once a constructor containing final field writes completes, and the reference to the object is published in a safe way (such as through a final field, static initializer, or properly synchronized access), all threads will see the correctly initialized values of those final fields.

This prevents a common concurrency bug where another thread might see default values for fields set in the constructor, even though the constructor has supposedly completed. The key is avoiding 'this' escape during construction.

5. What is the 'this' escape issue and how do you prevent it?

'This' escape occurs when a reference to the object being constructed becomes visible to another thread before the constructor finishes executing. This typically happens by publishing the reference in a callback, starting a thread, or adding 'this' to a collection during construction.

Prevention strategies include: never register listeners or callbacks with 'this' during construction, avoid starting threads in constructors, use static factory methods or holder patterns for safe publication, and use final fields combined with proper initialization. The fix is to complete all initialization before publishing the reference.

6. What is the happens-before order guarantee provided by volatile fields?

A write to a volatile field happens-before every subsequent read of that same field. This means any variables written before the volatile write are guaranteed to be visible to reads that occur after the volatile read due to transitivity with the program order rule.

For example, if you write ready = true as volatile after writing data = compute(), any thread that reads ready as true is also guaranteed to see the computed data value.

7. How does the Java Memory Model handle reordering by the JVM and CPU?

The JMM permits reordering of memory operations for performance, but defines happens-before rules that prohibit certain reorderings. Within a single thread, program order is preserved. Across threads, happens-before relationships from locks, volatile, and thread start/join establish ordering constraints.

The key insight is that without synchronized or volatile, the JVM and CPU can legally reorder operations in ways that produce seemingly impossible behavior in concurrent code. The JMM formalizes when visibility is guaranteed.

8. Can you explain the relationship between synchronized and the happens-before relationship?

Synchronized establishes happens-before through the monitor lock rule: unlocking a monitor happens-before subsequent locking of the same monitor. This means all memory operations in a synchronized block become visible to the next thread that enters a synchronized block on the same monitor.

The unlock happens-before the lock, and combined with program order within each thread, this chains visibility guarantees across thread boundaries. This is why synchronized is so effective for correcting data races.

9. What is the "double-checked locking" idiom and why does it require volatile?

Double-checked locking attempts to reduce synchronization overhead for lazy singleton initialization. The first check without locking determines if initialization is needed; the second check inside synchronized prevents race conditions. Without volatile, the reference assignment can be reordered relative to constructor execution.

A thread might see a non-null reference but fields set to default values (zero, null, false) because the JVM assigned the reference before the constructor finished. Volatile prevents this reordering by establishing happens-before between the write and subsequent reads.

10. How do final fields interact with the Java Memory Model after construction?

The JMM provides special guarantees for final fields. Once a constructor finishes and the reference to the object is published through a final field, static initializer, or properly synchronized access, all threads are guaranteed to see the correctly initialized values of final fields—not their default values.

This prevents observation of partially constructed objects containing final fields. However, this guarantee requires that the reference does not escape during construction (no 'this' escape).

11. What is a data race and how does the Java Memory Model define it?

A data race occurs when there are multiple concurrent accesses to the same variable, at least one is a write, and the accesses are not ordered by happens-before relationships. The JMM states that a program has a data race if the behavior cannot be proven by considering only the program order and monitor lock rules.

The practical consequence is that without proper synchronization, memory operations can be reordered and visibility is not guaranteed, leading to unpredictable behavior. Synchronized, volatile, or other concurrency primitives establish the necessary ordering.

12. Explain the monitor lock rule in the Java Memory Model.

The monitor lock rule states that an unlock operation on a monitor happens-before every subsequent lock operation on the same monitor. This means when thread A unlocks a monitor and thread B later locks the same monitor, all memory operations performed by thread A before the unlock become visible to thread B after it acquires the lock.

This rule applies to both synchronized methods (which use the implicit monitor of the object) and synchronized blocks with explicit lock objects. The guarantee extends to all memory operations, not just those touching the locked object.

13. Can volatile variables be used to implement atomic operations like increment?

No. Volatile only guarantees visibility of individual reads and writes. Operations like counter++ involve three steps: read the current value, increment it, and write the new value. This read-modify-write sequence is not atomic even with volatile.

If two threads both read counter = 0 simultaneously, both might increment to 1 and write it back, resulting in lost updates. For atomic increment, use AtomicInteger.incrementAndGet() or synchronize the entire operation.

14. What is the relationship between the Thread Start rule and happens-before?

The Thread Start rule states that a call to Thread.start() happens-before any action in the started thread. This means all memory operations performed by the calling thread before start() are guaranteed to be visible to the new thread when it begins execution.

For example, if you initialize fields before calling start(), the started thread will see the initialized values, not default values. This rule ensures proper initialization visibility across thread boundaries.

15. How does the Java Memory Model handle the Thread Termination rule?

The Thread Termination rule states that a thread's termination action happens-before any code that detects the thread has terminated (via join(), isAlive(), or other means). When thread A calls threadB.join() and it returns normally, all memory operations performed by thread B are visible to thread A.

This rule is critical for safe result propagation: when a worker thread completes and you call join(), you are guaranteed to see all the work that thread accomplished, not just the final state.

16. What is the transitivity rule in happens-before relationships?

The transitivity rule states that if action A happens-before B, and B happens-before C, then A happens-before C. This chains together multiple happens-before relationships to establish ordering across complex synchronization patterns.

For instance, if a volatile write happens-before an unlock (through program order), and that unlock happens-before a subsequent lock (monitor rule), then the volatile write happens-before the lock acquisition, establishing visibility of the written value.

17. Why is it important to restore the interrupt flag after catching InterruptedException?

The interrupt flag represents a signal sent by another thread. When InterruptedException is thrown, the JVM automatically clears the interrupt flag. If you catch the exception without restoring the flag, the interrupt signal is lost forever.

Restoring the flag with Thread.currentThread().interrupt() preserves the signal so code higher up the call stack or in other threads can detect that an interrupt was requested. Failing to restore means the interrupt is silently swallowed, potentially causing threads to ignore shutdown requests.

18. What is the difference between the program order rule and the volatile field rule?

The program order rule applies within a single thread: each action happens-before any subsequent action in that same thread. This means reordering within a thread is constrained by source code order.

The volatile field rule applies across threads: a write to a volatile field happens-before every subsequent read of that same field. This rule specifically constrains visibility across threads for the volatile variable, not all operations in the thread.

Together, the program order rule within a writer thread and the volatile field rule at the reader thread create a happens-before chain that ensures the write is visible.

19. How does safe publication through a static initializer work with final fields?

Static initialization is performed by the JVM under a special lock that guarantees synchronized access. When a static field is initialized in a static initializer or assigned directly, the reference to the enclosing class is published in a way that establishes happens-before for all threads that access the field.

Combining this with final fields means: once the static holder instance is created, all final fields within it are guaranteed to be visible to any thread that accesses the instance through the static getter, without additional synchronization.

20. What happens if you call start() twice on the same Thread object?

Calling start() twice on the same Thread throws IllegalThreadStateException. After a thread has been started and reached TERMINATED state, it cannot be restarted. The JVM tracks the thread's state and enforces this constraint.

This is why creating a "restartable" thread pattern requires wrapping: either use an executor service that manages thread lifecycle, or implement a loop inside run() that processes multiple tasks without the thread ever terminating.

Further Reading

Conclusion

You now understand happens-before guarantees. Apply this knowledge to ensure your concurrent code is correct: use volatile for simple flags, synchronized for compound operations, and never let this escape during construction. When designing concurrent classes, sketch out the happens-before chain to verify that writes in one thread are visible to reads in another. Continue with Thread Lifecycle: States, start, yield, sleep, interrupt, join to understand how thread state transitions interact with the JMM.

Category

Related Posts

Java Thread Lifecycle: States, start, yield, sleep, interrupt, join

Understanding Java thread lifecycle management: thread states, start, yield, sleep, interrupt, join, daemon threads and common pitfalls.

#java #jvm #concurrency

Java Atomics and VarHandle: Low-Level Concurrency

Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.

#java #jvm #concurrency

Java Concurrent Collections: ConcurrentHashMap, BlockingQueue

Java concurrent collections deep dive: ConcurrentHashMap, BlockingQueue, CopyOnWriteArrayList, ConcurrentLinkedQueue, and choosing the right structure.

#java #jvm #concurrency