Week 8 Day 3 — Happens-before and Safe Publication
Goal
Today I want visibility as a Java Memory Model rule, not as “the CPU cache flushed.”
Main questions:
- What is happens-before?
- Why do people still see stale data?
- What is safe publication?
- Why are
finalfields special? - Why does double-checked locking need
volatile?
1. Happens-before
The JMM does not promise that thread B sees thread A’s writes just because A ran earlier on the clock.
If action A happens-before action B, B is guaranteed to see A’s writes (and those A had already seen).
Practical sources:
| Edge | Example |
|---|---|
| Program order in one thread | Obvious sequential code |
| Unlock then later lock of the same monitor | synchronized |
Write volatile then later read of that variable | stop flag |
Thread.start | new thread sees writes before start |
Thread termination then join | joiner sees the worker’s writes |
Constructor completion then safe publish of final fields | immutable objects |
Executor submit / Future.get | completion visibility |
Without an edge, B may see a torn object, a default 0, or an old value forever.
Memory sentence:
If there is no happens-before, there is no visibility guarantee.
2. Stale data is not “eventually”
People say the cache will catch up. The model allows a thread to never see another thread’s write if they never synchronize.
boolean ready = false; // not volatile
Object payload;
// thread A
payload = new Payload();
ready = true;
// thread B
if (ready) {
use(payload); // payload may still be null, or half-initialized
}
ready without volatile is not a publish. B can see ready == true and a null payload (reordering), or ready == false forever.
Thread.sleep is not a memory barrier I rely on.
3. Safe publication
Publishing an object to other threads means they must see it fully constructed.
Safe ways:
- static initializer (
static final Map = Map.of(...)) volatilefield- storing under a lock, reading under the same lock
- concurrent collections (
ConcurrentHashMap.puthappens-before a successfulget) finalfields after the constructor returns, if the reference is then safely published
Unsafe: writing a field of a shared object from a @PostConstruct thread and reading it from request threads with no volatile/lock/concurrent structure.
4. final fields
After the constructor finishes, final fields are visible to any thread that sees the published reference. That is why immutable objects (String, well-written records) are shareable.
Rules I keep:
- all fields
final - no
thisescape in the constructor (do not passthisto another thread before the constructor returns) - mutable internals copied (Week 1)
A record with an ArrayList component is not safely shareable if callers still mutate the list.
5. Double-checked locking
private volatile Expensive instance;
public Expensive get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Expensive();
}
}
}
return instance;
}
Without volatile on instance, a thread can see a non-null reference and a partially constructed Expensive. The inner lock publishes to threads that take the lock; the fast path does not take the lock, so it needs volatile.
In Spring I do not write this. The container publishes the singleton. I still need the idea for lazy fields I create myself.
Memory sentence:
Double-checked locking without
volatilecan publish a half-built object.
6. Spring connection
- The container constructs beans, then publishes them to the context. Request threads see fully built singletons. That is safe publication of the bean graph.
- A
HashMapI fill in@PostConstructand read from requests: if the field is a plainHashMapassigned once and never mutated after the method returns, publishing the field asvolatile(or making itfinaland filling in the constructor) is the story. If I keep putting into it later, I needConcurrentHashMap. @Lazydouble-checked inside Spring — I do not reimplement it.
7. Common traps
Trap 1: “volatile flushes to RAM” as the whole JMM.
Trap 2: Using sleep or “it works on my machine” as a barrier.
Trap 3: Double-checked locking without volatile.
Trap 4: Escaping this from a constructor (listeners.add(this) on another thread).
Trap 5: Filling a non-concurrent map from one thread and mutating it from others after publish.
Practice Questions and Answers
Question 1
What is happens-before? Why do people still see stale data?
Answer:
Happens-before is the JMM’s visibility contract: if A happens-before B, B sees A’s writes. Locks, volatile, thread start/join, and final after construction are typical edges. Without an edge, another thread can see old or torn state indefinitely. Clock order is not enough.
Question 2
Why are final fields special after the constructor returns?
Answer:
The JMM guarantees that a thread which sees the published object sees those final fields as assigned in the constructor. That is the foundation of immutable objects. The reference itself still needs to be published safely, and I must not leak this during construction.
Question 3
What is safe publication?
Answer:
Making an object visible to other threads so they see it fully initialized. Static init, volatile, the same lock, or a concurrent collection. A plain field write is not enough.
Question 4
Is Thread.sleep a memory barrier?
Answer:
Not one I rely on. Sleeping may happen to let another thread run; it is not a happens-before edge for my fields.
Question 5
How does ConcurrentHashMap help publication?
Answer:
A successful put happens-before a get that sees that key. I can publish values through the map without a separate volatile on each value, as long as I do not mutate the value objects unsafely afterwards.
Memory sentences
If there is no happens-before, there is no visibility guarantee.
Safe publication: volatile, lock, concurrent collection, or static init.
Double-checked locking without
volatilecan publish a half-built object.