🧵 Section 9 · Question #2

What is happens-before relationship

The Happens-Before relationship is the foundational partial-order relation defined in the Java Memory Model (JMM / JSR-133) that specifies memory visibility guarantees across mu...


🟢 Junior Level

30-Second Summary

The Happens-Before relationship is the foundational partial-order relation defined in the Java Memory Model (JMM / JSR-133) that specifies memory visibility guarantees across multiple threads.

If action $A$ happens-before action $B$ ($A \to B$), then the memory updates produced by action $A$ are guaranteed to be visible to action $B$, and action $B$ will observe the state left by action $A$.

[!IMPORTANT] Happens-Before does NOT dictate physical wall-clock execution time. It does not require action $A$ to complete before action $B$ on the CPU clock. The JIT compiler and CPU hardware remain free to reorder instructions for performance optimization, provided the observable outcome for all threads is consistent with the order established by the happens-before relationship.


Java 21 Code Examples

public class HappensBeforeBasicsDemo {

    private int data = 0;
    private volatile boolean ready = false;

    // Executed by Thread 1:
    public void writer() {
        data = 42;    // Action A (Ordinary store)
        ready = true; // Action B (Volatile store)
    }

    // Executed by Thread 2:
    public void reader() {
        if (ready) {  // Action C (Volatile load)
            // Action D: data is guaranteed to evaluate to 42!
            System.out.println("Data: " + data);
        }
    }
}

Why This Works Under JMM:

  1. $A \to B$ (Program Order Rule): In Thread 1, writing data = 42 happens-before writing ready = true.
  2. $B \to C$ (Volatile Variable Rule): Writing to a volatile field happens-before every subsequent read of that same field.
  3. $A \to C$ (Transitivity Rule): Because $A \to B$ and $B \to C$, action $A$ happens-before action $C$. Thread 2 is mathematically guaranteed to observe data == 42.

🟡 Middle Level

The 8 Fundamental Happens-Before Rules (JSR-133)

The Java Memory Model formalizes concurrency around eight specific rules:

# Rule Name Specification Contract
1 Program Order In a single thread, each action happens-before every action that follows it in program code order.
2 Monitor Lock An unlock on an intrinsic monitor (monitorexit / exiting synchronized) happens-before every subsequent lock (monitorenter) on the same monitor.
3 Volatile Variable A write to a volatile field happens-before every subsequent read of the same volatile field.
4 Thread Start A call to Thread.start() on a thread instance happens-before any action in the started thread’s run() method.
5 Thread Termination (Join) Any action in a thread happens-before any other thread successfully returns from a Thread.join() on that thread or observes false from isAlive().
6 Interruption A thread calling targetThread.interrupt() happens-before the target thread discovers the interruption (via isInterrupted(), Thread.interrupted(), or throwing InterruptedException).
7 Finalizer The end of an object’s constructor happens-before the execution of that object’s finalize() method starts.
8 Transitivity If action $A$ happens-before action $B$ ($A \to B$), and action $B$ happens-before action $C$ ($B \to C$), then action $A$ happens-before action $C$ ($A \to C$).

The Piggybacking Technique

Thanks to the Transitivity Rule, developers can establish memory visibility for an entire group of normal (non-volatile) fields by publishing a single volatile guard variable:

public class PiggybackingDemo {
    private int field1;
    private String field2;
    private double field3;
    private volatile boolean published = false;

    public void publishData() {
        field1 = 100;
        field2 = "Java 21";
        field3 = 3.14;
        published = true; // Volatile write flushes all preceding non-volatile writes via StoreStore barrier
    }

    public void consumeData() {
        if (published) { // Volatile read pulls fresh CPU caches via LoadLoad/LoadStore barriers
            System.out.printf("%d, %s, %.2f%n", field1, field2, field3); // Guaranteed to print fresh values!
        }
    }
}

🔴 Senior Level

final Field Semantics & Final Field Freeze

The JMM defines a dedicated safety guarantee for final fields to prevent other threads from observing partially constructed state:

The values of an object’s final fields initialized in its constructor are considered frozen at the moment the constructor completes. Any thread obtaining a reference to that object after constructor termination is guaranteed to observe the finalized values without explicit synchronization.

public class ImmutableState {
    private final int id;
    private final String payload;

    public ImmutableState(int id, String payload) {
        this.id = id;
        this.payload = payload;
        // At the end of the constructor, the JIT inserts an implicit Freeze Action (StoreStore barrier)
    }
}

The Fatal Exception: Escaping this Reference

If a reference to the constructing object escapes to another thread before the constructor exits:

public class BrokenEscape {
    private final int value;
    public static BrokenEscape instance;

    public BrokenEscape() {
        this.value = 42;
        instance = this; // ❌ ESCAPING REFERENCE! Breaks the JMM final freeze guarantee!
    }
}

Another thread accessing instance concurrently may read the reference before the constructor’s freeze action has executed, observing default uninitialized field state (value == 0).


Hardware Architectures: x86 TSO vs. ARM Weak Memory

The concrete machine instructions emitted to satisfy happens-before guarantees depend heavily on the underlying CPU memory model:

  1. x86 / x86-64 (Total Store Order — TSO):
    • Hardware enforces order for LoadLoad, StoreStore, and LoadStore natively.
    • The only hardware-permitted reordering is that loads may be reordered before older stores to different locations (StoreLoad).
    • Consequently, on x86, LoadLoad, StoreStore, and LoadStore barriers are zero-cost no-ops (compiled into regular MOV instructions). Only the StoreLoad barrier requires an expensive LOCK ADD or MFENCE instruction.
  2. ARM / Apple Silicon / POWER (Weakly Ordered):
    • The hardware permits aggressive reordering of virtually all memory read and write sequences.
    • To enforce happens-before, the JIT compiler must insert explicit hardware memory barriers: dmb ishld (for load fences) and dmb ish (for store fences), making volatile and synchronized coordination architecturally more expensive than on x86.

Empirical Validation with JCStress

Unit tests cannot reliably detect happens-before violations because cache timing discrepancies and race conditions on modern multi-core CPUs are notoriously hard to trigger deterministically.

Production concurrency frameworks (e.g. Netty, Caffeine, OpenJDK) validate JMM compliance using JCStress (Java Concurrency Stress Kit):

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

@JCStressTest
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Both stores visible")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Partial update")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE, desc = "Reordered read observed")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Race condition violation")
@State
public class MemoryReorderingStressTest {
    int x, y;

    @Actor
    public void actor1() {
        x = 1;
        y = 1;
    }

    @Actor
    public void actor2(II_Result r) {
        r.r1 = y;
        r.r2 = x;
    }
}

4 Tricky Questions

1. Does $A \text{ happens-before } B$ imply that action $A$ must physically execute before action $B$ in wall-clock time?

Answer: No. Happens-Before is strictly a formal memory visibility guarantee, not an instruction execution scheduler.

If action $A$ and action $B$ do not depend on each other and their reordering produces an identical observable state across all threads, the JIT compiler and CPU out-of-order execution engine are permitted to execute action $B$ prior to action $A$. Happens-before merely guarantees that when action $B$ reads state written by action $A$, the effects of $A$ are fully visible to $B$.


2. What is “Piggybacking” on a happens-before relationship, and how does it work?

Answer: Piggybacking is the practice of leveraging the Transitivity Rule and the Program Order Rule to safely publish a collection of ordinary, non-volatile variables using a single synchronization operation.

A thread modifies multiple non-volatile fields first, and as its final step writes to a volatile variable. Due to program order, all non-volatile stores happen-before the volatile write. When another thread reads that same volatile variable, volatile semantics ensure that all preceding stores are made visible to the reader through transitivity.


3. What is the Final Field Freeze rule, and how can leaking the this reference violate it?

Answer: Under JSR-133, the JIT compiler inserts an implicit memory barrier (freeze action) at the end of every constructor, guaranteeing that all final fields are committed to memory before the newly constructed object reference is published.

However, if the this pointer escapes the constructor prematurely (such as by assigning this to a static field, publishing to an event listener, or starting a thread within the constructor), other threads can obtain a reference to the uncompleted object before the freeze action executes. Those threads may then observe uninitialized default values (0 or null) in fields declared as final.


4. Why do memory barriers cost significantly more on ARM processors than on x86 architectures?

Answer: x86 processors implement a hardware memory model called Total Store Order (TSO), which guarantees in hardware that loads and stores are not reordered against each other (except for StoreLoad reordering). As a result, three of the four standard JMM memory barriers (LoadLoad, StoreStore, and LoadStore) are completely free on x86 and require no special hardware instructions.

In contrast, ARM processors use a Weak Memory Model where hardware is free to reorder almost any independent load and store instructions. Enforcing happens-before ordering on ARM requires the JIT compiler to emit explicit data memory barrier instructions (dmb ishld, dmb ish), which stall execution pipelines and flush hardware queues.


🎯 Interview Cheat Sheet

8 Rules Summary Matrix

| Rule Name | Source Event ($A$) | Destination Event ($B$) | | :— | :— | :— | | Program Order | Earlier statement in thread | Later statement in same thread | | Monitor Lock | Unlock monitor (monitorexit) | Subsequent lock on same monitor (monitorenter) | | Volatile | Store to volatile V | Subsequent load from same volatile V | | Thread Start | Call to t.start() | First instruction in t.run() | | Thread Termination | Last instruction in t | Return from t.join() or isAlive() == false | | Interruption | Call to t.interrupt() | Target thread observes interruption status | | Finalizer | End of constructor execution | Invocation of finalize() | | Transitivity | If $A \to B$ and $B \to C$ | Then $A \to C$ is guaranteed |


Key Takeaways

  • Visibility Contract: Happens-before guarantees memory visibility, not physical execution timing.
  • Transitivity Power: Chains of happens-before edges allow piggybacking non-volatile state onto a single volatile write.
  • Final Field Safety: final fields are guaranteed safe publication via the constructor freeze action, provided this does not escape.
  • Hardware Variation: x86 TSO makes most memory barriers free; ARM requires explicit DMB instructions.

Red Flags & Anti-Patterns to Avoid

  • ❌ Equating happens-before with wall-clock execution time: Misunderstands compiler reordering and JMM specs.
  • ❌ Leaking this from constructors: Destroys final field freeze guarantees, leading to partial object observation.
  • ❌ Assuming plain fields are visible across threads without an HB edge: Causes catastrophic visibility bugs in multi-core production systems.
  • ❌ Relying on Thread.sleep() to synchronize memory: Sleep creates a time delay, but establishes zero happens-before edges.