Mastering Java’s Happens‑Before: Unlock Visibility & Ordering in Multithreading
This article explains Java’s happens‑before rules, covering visibility, ordering, program order, volatile, monitor locks, start/join semantics, and provides concrete code examples to illustrate how these rules prevent common concurrency bugs.
Solution
To write bug‑free concurrent code, developers need a memory model that is easy to understand and program against. This leads to two contrasting models: a strong memory model for programmers and a weak memory model for compilers and processors, allowing them to perform optimizations.
“There’s nothing a meeting can’t solve; if there is, schedule another meeting.” 😂
JSR‑133 experts propose a compromise: selectively disable caching and compiler optimizations using volatile , synchronized , final and the Happens‑Before principle.
The Java Memory Model (JMM) tells the JVM to obey the happens‑before rules while secretly applying its own strategies.
For reorderings that would change program results, the JMM requires compilers and processors to forbid them.
For reorderings that don’t affect results, the JMM permits them.
Happens‑before
The happens‑before rule constrains two operations: it does not require the first operation to execute before the second, only that the result of the first be visible to the second.
Consider the following code:
class ReorderExample {
int x = 0;
boolean flag = false;
public void writer() {
x = 42; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
System.out.println(x); //4
}
}
}If thread A runs writer and thread B runs reader, x may be printed as 0 because operations 1 and 2 have no data dependency and can be reordered.
Adding volatile to flag changes the semantics: volatile boolean flag = false; Since Java 1.5, volatile gains stronger memory semantics that, together with the happens‑before rules, guarantee visibility and ordering.
Program Order Rule
In a single thread, each operation happens‑before any subsequent operation in that thread.
This rule reflects the “as‑if‑serial” semantics: as long as the result is unchanged, the actual execution order may vary.
Volatile Variable Rule
A write to a volatile field happens‑before any subsequent read of that field.
Example:
public class ReorderExample {
private int x = 0;
private int y = 1;
private volatile boolean flag = false;
public void writer() {
x = 42; //1
y = 50; //2
flag = true; //3
}
public void reader() {
if (flag) { //4
System.out.println("x:" + x); //5
System.out.println("y:" + y); //6
}
}
}Operations 1 and 2 cannot be reordered after the volatile write (3), but they may be reordered with each other.
Monitor Lock Rule
Unlocking a lock happens‑before any subsequent locking of that same lock.
This explains the semantics of synchronized:
public class SynchronizedExample {
private int x = 0;
public void synBlock() {
synchronized (SynchronizedExample.class) {
x = 1; // write
}
}
public synchronized void synMethod() {
x = 2; // write
}
}After a thread releases the lock, another thread acquiring the same lock will see the updated value of x.
start() Rule
If thread A calls ThreadB.start() , that call happens‑before any action in thread B.
public class StartExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
StartExample startExample = new StartExample();
Thread thread1 = new Thread(startExample::writer, "线程1");
startExample.x = 10;
startExample.y = 20;
startExample.flag = true;
thread1.start();
System.out.println("主线程结束");
}
public void writer() {
System.out.println("x:" + x);
System.out.println("y:" + y);
System.out.println("flag:" + flag);
}
}The child thread sees all writes performed before start().
join() Rule
If thread A successfully returns from ThreadB.join() , all actions in thread B happen‑before the return in thread A.
public class JoinExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
JoinExample joinExample = new JoinExample();
Thread thread1 = new Thread(joinExample::writer, "线程1");
thread1.start();
thread1.join();
System.out.println("x:" + joinExample.x);
System.out.println("y:" + joinExample.y);
System.out.println("flag:" + joinExample.flag);
System.out.println("主线程结束");
}
public void writer() {
this.x = 100;
this.y = 200;
this.flag = true;
}
}After join() returns, the main thread observes the writes performed by the child thread.
Summary
Happens‑before ensures that the result of a preceding operation is visible to the following operation, solving visibility and ordering issues in multithreaded programs.
The start and join rules provide communication mechanisms between parent and child threads.
From a memory‑semantic perspective, a volatile write‑read pair has the same effect as a lock’s release‑acquire pair, while synchronized additionally guarantees atomicity.
Transitivity Rule
If A happens‑before B and B happens‑before C, then A happens‑before C.
Illustrated by the diagram:
From the diagram we see that x = 42 and y = 50 happen‑before flag = true (Rule 1), and flag = true happens‑before the read of flag (Rule 2). By transitivity, the writes to x and y happen‑before the read of flag, guaranteeing their visibility.
Further Questions
What are the differences in CPU instructions generated by synchronized blocks versus synchronized methods?
How do daemon and non‑daemon threads affect JVM shutdown behavior?
What remaining doubts do you have about the happens‑before rules?
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
