The Java Memory Model
happens-before rules: locks, volatile, thread start/join, final fields.
A contract, not a machine
The Java Memory Model (JMM) defines when a write in one thread is guaranteed to be visible in another. The core idea is happens-before: if action A happens-before action B, then B sees A's effects. With no edge there's no guarantee: a reader may see a stale value indefinitely, even long after the write.
The four edges you'll use daily
1. Unlocking monitor M → every later lock of M. 2. A write to volatile v → every later read of v. 3. t.start() → every action inside t. 4. Every action inside t → another thread returning from t.join(). Locks, volatile, start and join are safe ways to pass data between threads because of these edges.
Your turn
main writes data[0] = 7, then starts the worker. What is printed?
int[] data = {0};
data[0] = 7;
Thread t = new Thread(() ->
System.out.println(data[0] + 1));
t.start();
t.join();81It varies between runs
Show the answer
Always 8. Everything main did before t.start() happens-before every action in t, so the worker is guaranteed to see 7. No volatile needed.
Is a minute long enough?
Thread A writes a plain field. Thread B reads it a full minute later. Is B guaranteed to see the new value?
Think about it, then reveal the answer
No. Plain field access creates no happens-before edge, and without one the JMM allows B to see the old value forever. "It usually works" isn't a guarantee. Locks, volatile, start() and join() are what create edges.
final fields: free safe publication
Fields marked final and set in a constructor are visible to all threads once construction finishes, with no locks needed. One condition: this must not escape during construction, for example by registering a listener or starting a thread from the constructor.
Broken double-checked locking
The first instance == null check runs without the lock, so it has no happens-before edge with the write. Another thread may see the reference *before* the constructor's writes: a half-built Helper. Fix: declare instance volatile, or use the holder-class idiom.
private static Helper instance; // bug
static Helper get() {
if (instance == null) { // no lock!
synchronized (Helper.class) {
if (instance == null)
instance = new Helper();
}
}
return instance;
}Read the fine print
Open the javadoc of almost any java.util.concurrent class and you'll find a "Memory consistency effects" section. For example, putting a key into a ConcurrentHashMap happens-before a later read of that key. And "explain happens-before" plus double-checked locking are interview classics.
Key takeaways
- No happens-before edge = no visibility guarantee
- Unlock → later lock of the same monitor; volatile write → later read
- start() publishes earlier writes; join() sees the thread's writes
- final fields are safe after construction if this doesn't escape
Java's original memory model was broken: it couldn't even make final fields reliable. JSR-133 rewrote it for Java 5 in 2004, and that model is still in force today.
Practice questions
What does this print?
int[] data = {0};
data[0] = 5;
Thread t = new Thread(() ->
System.out.println(data[0] * 2));
t.start();
t.join();- 10
- 0
- 5
- It varies between runs
Check your answer
10. Everything main did before t.start() happens-before every action in t. So the worker is guaranteed to see 5.
Which of these does NOT create a happens-before edge between two threads?
- Both threads reading and writing the same plain field
- Thread A releasing a lock that Thread B then acquires
- Thread A writing a volatile field that Thread B then reads
- Main returning from t.join() after t finishes
Check your answer
Both threads reading and writing the same plain field. Plain field access gives no ordering or visibility guarantees. Locks, volatile and join() all create happens-before edges.