If you remember only one formal concurrency concept from the Java Memory Model, remember happens-before.
It is the rule that turns “I think another thread should see this” into “the model guarantees another thread can see this.”
Problem Statement
Suppose thread A:
- updates shared state
- then sets a signal
Thread B sees the signal. Can it safely assume the state update is also visible?
Without a happens-before edge, not necessarily.
That is the whole point.
Naive Mental Model
The naive model is:
- if A did it earlier in source code, B sees it later
That is not a safe concurrency rule.
Happens-before is stronger. It says:
- if action A happens-before action B, then B is guaranteed to observe the effects of A in the required visibility and ordering sense
This is the practical bridge between synchronization and reasoning.
Why Happens-Before Matters
You rarely debug concurrency at the level of CPU caches directly. You debug it by asking:
- what synchronization exists?
- what does that synchronization guarantee?
- is there a happens-before edge between the writer and the reader?
If the answer is no, your code is relying on timing.
Common Happens-Before Sources
The most important ones for application code are:
- monitor unlock -> later lock on the same monitor
- volatile write -> later volatile read of the same variable
- actions before
Thread.start()-> actions inside the started thread - actions in a thread before completion -> actions after another thread successfully
join()s it
These are the edges you will use repeatedly in real Java code.
Runnable Example: Volatile Signal
public class HappensBeforeVolatileDemo {
private static int data;
private static volatile boolean ready;
public static void main(String[] args) throws Exception {
Thread writer = new Thread(() -> {
data = 42;
ready = true;
}, "writer");
Thread reader = new Thread(() -> {
while (!ready) {
// wait
}
System.out.println("Observed data = " + data);
}, "reader");
reader.start();
writer.start();
writer.join();
reader.join();
}
}
Why this works:
- the write to
readyis volatile - the later read of
readyis volatile - that establishes a happens-before edge
- once the reader sees
ready == true, it can also safely observe the earlier write todata
This is one of the most important visibility patterns in Java.
Runnable Example: join as a Visibility Boundary
public class HappensBeforeJoinDemo {
private static int result;
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> result = 99, "worker");
worker.start();
worker.join();
System.out.println("Result after join = " + result);
}
}
Why this works:
- the worker’s actions before completion happen-before the main thread continues after
join
That means join is not only waiting.
It is also a visibility boundary.
Broken Shape Without a Real Edge
public class BrokenSignalDemo {
private static int data;
private static boolean ready;
public static void main(String[] args) throws Exception {
Thread writer = new Thread(() -> {
data = 42;
ready = true;
});
Thread reader = new Thread(() -> {
while (!ready) {
// spin
}
System.out.println(data);
});
reader.start();
writer.start();
}
}
Why this is unsafe:
- there is no guaranteed happens-before edge between the writer’s plain writes and the reader’s plain reads
It may work. That is not the same as being correct.
Production-Style Example
Imagine a cache refresher thread:
- loads latest pricing rules
- validates them
- publishes the new snapshot
Request threads then:
- read the published reference
- make pricing decisions
The key design question is: what publication step creates the happens-before edge?
Good answers:
- volatile reference write
- atomic reference set
- publishing under a lock and reading under the same lock
Bad answer:
- “the refresher runs first most of the time”
That is timing, not correctness.
Performance and Trade-Offs
Happens-before is not a separate API. It is a property created by correct synchronization.
That means performance questions become design questions:
- do you need a lock because multiple variables must move together?
- is a volatile signal enough because only visibility matters?
- can immutable snapshots reduce the synchronization surface?
Good concurrency design is often just good happens-before design.
Testing and Debugging Notes
In code reviews, try asking:
- where is the writer?
- where is the reader?
- what exact happens-before edge connects them?
If nobody can answer, the code is probably relying on luck.
This question is so useful that it should become a habit.
Decision Guide
- use volatile when you need a simple visibility edge
- use locks when several operations must become visible as one protected critical section
- use
joinand task completion boundaries intentionally, not accidentally
The core idea stays the same: concurrent correctness needs an explicit edge.
Key Takeaways
- happens-before is the practical visibility and ordering rule in Java concurrency
- without it, shared-state reasoning is unreliable
- volatile, locks, start, and join all matter because they create happens-before edges