Skip to main content

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:

  1. What is happens-before?
  2. Why do people still see stale data?
  3. What is safe publication?
  4. Why are final fields special?
  5. 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:

EdgeExample
Program order in one threadObvious sequential code
Unlock then later lock of the same monitorsynchronized
Write volatile then later read of that variablestop flag
Thread.startnew thread sees writes before start
Thread termination then joinjoiner sees the worker’s writes
Constructor completion then safe publish of final fieldsimmutable objects
Executor submit / Future.getcompletion 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(...))
  • volatile field
  • storing under a lock, reading under the same lock
  • concurrent collections (ConcurrentHashMap.put happens-before a successful get)
  • final fields 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 this escape in the constructor (do not pass this to 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 volatile can 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 HashMap I fill in @PostConstruct and read from requests: if the field is a plain HashMap assigned once and never mutated after the method returns, publishing the field as volatile (or making it final and filling in the constructor) is the story. If I keep putting into it later, I need ConcurrentHashMap.
  • @Lazy double-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 volatile can publish a half-built object.

Next: Week 8 Day 4 — Executors and CompletableFuture