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:
- $A \to B$ (Program Order Rule): In Thread 1, writing
data = 42happens-before writingready = true. - $B \to C$ (Volatile Variable Rule): Writing to a
volatilefield happens-before every subsequent read of that same field. - $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
finalfields 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:
- x86 / x86-64 (Total Store Order — TSO):
- Hardware enforces order for
LoadLoad,StoreStore, andLoadStorenatively. - The only hardware-permitted reordering is that loads may be reordered before older stores to different locations (
StoreLoad). - Consequently, on x86,
LoadLoad,StoreStore, andLoadStorebarriers are zero-cost no-ops (compiled into regularMOVinstructions). Only theStoreLoadbarrier requires an expensiveLOCK ADDorMFENCEinstruction.
- Hardware enforces order for
- 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) anddmb 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:
finalfields are guaranteed safe publication via the constructor freeze action, providedthisdoes not escape. - Hardware Variation: x86 TSO makes most memory barriers free; ARM requires explicit
DMBinstructions.
Red Flags & Anti-Patterns to Avoid
- ❌ Equating happens-before with wall-clock execution time: Misunderstands compiler reordering and JMM specs.
- ❌ Leaking
thisfrom constructors: Destroysfinalfield 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.