Definition
- Happens-before is a formal ordering relationship defined by the Java Memory Model (JSR-133) that guarantees memory visibility between threads.
- Without it, the JVM, JIT compiler, and CPU are free to cache variables in registers, reorder instructions, and delay writes to main memory. A reading thread may never see updates made by another thread.
- Seven rules establish happens-before edges: (1) program order within a single thread, (2) monitor unlock happens-before subsequent lock on the same monitor, (3) volatile write happens-before subsequent read of the same volatile, (4) Thread.start() happens-before the new thread's first action, (5) a thread's last action happens-before Thread.join() returns, (6) constructor completion happens-before finalizer and final field freeze, (7) transitivity: if A HB B and B HB C, then A HB C.
- Transitivity is the most practically important rule: a volatile write makes all prior writes (including non-volatile ones) visible to any thread that subsequently reads the volatile variable.
Think of a relay race at National Bank of India's annual sports day. Each runner (thread) carries a baton (happens-before edge). When Rajesh passes the baton to Priya at the exchange zone (synchronized unlock/lock or volatile write/read), everything Rajesh wrote in the ledger is guaranteed to be visible to Priya. Without the baton pass, Priya might be reading her own copy of the ledger that was never updated. The baton is the happens-before edge that ensures one runner's work is visible to the next.
Understanding the Concepts
What is happens-before?
Happens-before is a formal guarantee defined by the Java Memory Model. If action A happens-before action B, then every write performed by A (and all writes that happen-before A, transitively) is visible to B. Without this guarantee, the JVM makes no promises about cross-thread visibility. A thread can read a stale value from its CPU cache and never see the updated value written by another thread.
Why do threads read stale values without happens-before?
Modern CPUs have per-core caches (L1, L2) and store buffers. When a thread writes to a variable, the write may sit in the store buffer or L1 cache and never be flushed to main memory. Another thread reading the same variable on a different core reads from its own cache, which still holds the old value. The JIT compiler makes this worse by hoisting variable reads out of loops and caching them in a CPU register.
package com.bank.concurrency;
public class StaleReadDemo {
private static boolean configLoaded = false; // NOT volatile
private static String interestRate = "0";
public static void main(String[] args) {
new Thread(() -> {
interestRate = "7.5";
configLoaded = true; // no happens-before!
}, "ConfigLoader").start();
new Thread(() -> {
while (!configLoaded) {
// JIT may hoist the read of configLoaded out of the loop
// into a register. Thread spins forever seeing false.
}
// Even if we get here, interestRate might still be "0"
// because there is no happens-before for interestRate either.
System.out.println("Rate: " + interestRate);
}, "Worker").start();
}
}
What are all seven happens-before rules?
Rule 1: Program Order. Within a single thread, each statement happens-before the next statement in program order. This ensures that single-threaded behavior is predictable, even though the JIT may reorder instructions that have no data dependency.
Rule 2: Monitor Lock. An unlock on a monitor happens-before every subsequent lock on that same monitor. When Thread-1 exits a synchronized block and Thread-2 enters a synchronized block on the same lock, Thread-2 sees all writes made by Thread-1 before the unlock.
package com.bank.concurrency;
import java.math.BigDecimal;
public class MonitorHBDemo {
private static BigDecimal balance = BigDecimal.ZERO;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
synchronized (lock) {
balance = new BigDecimal("50000.00"); // write inside sync
} // UNLOCK: happens-before subsequent lock
});
writer.start();
writer.join();
synchronized (lock) { // LOCK: sees all writes before previous unlock
System.out.println("Balance: " + balance); // guaranteed 50000.00
}
}
}
Rule 3: Volatile. A write to a volatile variable happens-before every subsequent read of that same volatile variable. This is the foundation of the volatile flag pattern.
package com.bank.concurrency;
public class VolatileHBDemo {
private static volatile boolean ready = false;
private static int data = 0; // NOT volatile
public static void main(String[] args) {
new Thread(() -> {
data = 42; // non-volatile write
ready = true; // volatile WRITE: happens-before
}).start();
new Thread(() -> {
if (ready) { // volatile READ: sees the write
// By TRANSITIVITY: data write HB ready write HB ready read
System.out.println(data); // guaranteed 42
}
}).start();
}
}
Rule 4: Thread.start(). Calling thread.start() happens-before any action in the started thread. All writes before start() are visible to the new thread.
Rule 5: Thread.join(). All actions in a thread happen-before join() returns in the calling thread. After join(), all writes from the completed thread are visible.
package com.bank.concurrency;
public class StartJoinHBDemo {
private static int result = 0;
public static void main(String[] args) throws InterruptedException {
result = 10; // write before start()
Thread worker = new Thread(() -> {
// Rule 4: sees result = 10 (start() HB thread body)
result = result + 32;
});
worker.start();
worker.join();
// Rule 5: sees result = 42 (thread body HB join return)
System.out.println("Result: " + result); // guaranteed 42
}
}
Rule 6: Final Fields. The end of a constructor happens-before the freeze of final fields. Any thread that sees a properly published reference to the object will see the fully initialized final fields.
Rule 7: Transitivity. If A happens-before B, and B happens-before C, then A happens-before C. This is the most powerful rule because it chains visibility across multiple operations.
How does the volatile flag pattern use transitivity?
The volatile flag pattern is the most common application of transitivity. You write non-volatile data fields, then write a volatile flag. A reader checks the volatile flag, and if it sees the write, transitivity guarantees that all prior non-volatile writes are also visible.
package com.bank.concurrency;
import java.math.BigDecimal;
public class VolatileFlagPattern {
private BigDecimal balance; // NOT volatile
private String accountName; // NOT volatile
private volatile boolean published = false; // volatile flag
public void initialize() {
balance = new BigDecimal("75000.00"); // write 1
accountName = "Rajesh Savings SAV2001"; // write 2
published = true; // volatile write: HB edge
// Transitivity: write1 HB write2 HB volatile-write
}
public void read() {
if (published) { // volatile read: sees the write
// Transitivity: volatile-read HB means we see write1 and write2
System.out.println(accountName + ": " + balance);
// Guaranteed to see fully initialized values
}
}
}
What are common mistakes with happens-before?
The most common mistake is assuming that volatile on one variable provides visibility for all variables. Volatile only creates a happens-before edge for the specific volatile variable. The visibility of other variables depends on transitivity: they must have been written BEFORE the volatile write in program order.
// WRONG: writing non-volatile AFTER volatile does not help
volatile boolean flag = false;
int data = 0;
// Thread-1:
flag = true; // volatile write
data = 42; // AFTER volatile: NOT covered by happens-before!
// Thread-2:
if (flag) {
// data may still be 0! The write to data is AFTER the volatile write
// so transitivity does NOT apply.
}
// CORRECT: write non-volatile BEFORE volatile
data = 42; // BEFORE volatile write
flag = true; // volatile write creates HB edge covering data write
What should a senior engineer know?
Senior engineers should understand that happens-before is a partial order, not a total order. Two actions from different threads that have no happens-before relationship between them can appear in any order to any observer. The JMM does not define a global clock or a single timeline of events across threads. This means that reasoning about concurrent programs requires identifying the happens-before edges explicitly, not relying on intuition about timing.
What are common interview follow-up questions?
- Does happens-before mean the action physically happens before? No. It is a visibility guarantee, not a temporal ordering. The JIT may execute the instructions in any order as long as the happens-before contract is preserved for other threads.
- What happens if you read without a happens-before edge? The JMM permits any value that was ever written to the variable, or the default value, to be returned.
- Is happens-before transitive across different volatile variables? No. Transitivity only applies along the happens-before chain of the same edges. Writing volatile-A and reading volatile-B creates no relationship unless there is a chain connecting them.
Visual Overview
Happens-Before Rules Summary
════════════════════════════
Rule 2: Monitor (synchronized)
Thread-1: write(data) → UNLOCK(lock)
│ HB edge
Thread-2: LOCK(lock) → read(data) ← sees write
Rule 3: Volatile
Thread-1: write(data) → volatile write(flag)
│ HB edge
Thread-2: volatile read(flag) → read(data) ← sees write
Rule 7: Transitivity
A ──HB──→ B ──HB──→ C
Therefore: A ──HB──→ C
data=42 ─HB(program order)─→ flag=true ─HB(volatile)─→ if(flag)
Therefore: data=42 ─HB(transitivity)─→ if(flag) → read(data)=42
Key Takeaway
Happens-before is the JMM's visibility guarantee. If action A happens-before action B, all writes performed by A are visible to B. Without happens-before, a thread can read stale values forever because the JIT may cache variables in CPU registers and never read main memory again. Seven rules establish happens-before edges: program order, monitor unlock/lock, volatile write/read, Thread.start(), Thread.join(), final field freeze, and transitivity.
Transitivity is the most practically important rule. The volatile flag pattern exploits it: write non-volatile data fields, then write a volatile flag. A reader that sees the volatile write also sees all prior non-volatile writes through transitivity. This is how safe publication works without synchronizing every field individually.
At National Bank of India, a configuration reload flag was implemented without volatile. Worker threads cached the flag in a CPU register and never saw the update, causing them to use the old interest rate indefinitely. Adding volatile to the flag established the happens-before edge, and through transitivity, all configuration values became immediately visible to worker threads.
Remember one sentence: happens-before guarantees that one thread's writes are visible to another, and without it the JVM is free to show stale values forever.
---