Guide contents

Lesson 33 of 71

Safe Publication and final Fields

This lesson covers publication: making an object reachable from code that runs in another thread. After it, a reader can tell whether a shared object is published safely, write a correct lazy singleton, and explain which memory barriers the JVM emits on x86 and ARM. It builds on the rules from Happens-Before and volatile.

Idea Short meaning
Publication Storing a reference where another thread can read it
Escape Publishing an object before its construction finishes
final field semantics Values of final fields are visible once the constructor ends, even through a racy reference
Safe publication Publishing through an action that creates a happens-before edge
Double-checked locking Lazy init that skips the lock after the first call; needs volatile
Holder-class idiom Lazy init through class initialization
Memory barrier A CPU or compiler instruction that forbids some reorderings

Example 1: unsafe publication

An object stored in a plain field without any happens-before edge is published unsafely. The block is compiled but not run, because its output is not fixed and it may never terminate.

Java
public class UnsafePublication {
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    }

    static Point shared;   // plain field

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            Point p;
            while ((p = shared) == null) {
                Thread.onSpinWait();
            }
            System.out.println(p.x + "," + p.y);
        });
        reader.start();
        shared = new Point(1, 2);
        reader.join();
    }
}

The outcome is nondeterministic by the JMM. The allocation, the writes to x and y, and the write of the reference to shared can be reordered, so the reader is allowed to print 1,2, 0,2, 1,0, or 0,0. The loop is also allowed to spin forever, because shared is a plain field and its read can be hoisted. On x86 with HotSpot the program usually prints 1,2, which is why such bugs survive testing and appear on ARM servers.

Example 2: escaping this from a constructor

this escapes when a constructor hands the object to other code before all fields are set. The bug is visible even in a single thread.

Java
import java.util.ArrayList;
import java.util.List;

public class ThisEscape {
    interface Listener { void onEvent(String e); }

    static final List<Listener> registry = new ArrayList<>();

    static class AuditListener implements Listener {
        private final String prefix;

        AuditListener() {
            registry.add(this);          // this escapes here
            fire("constructing");        // callback runs before prefix is set
            prefix = "audit";
        }

        public void onEvent(String e) { System.out.println(prefix + ": " + e); }
    }

    static void fire(String e) {
        for (Listener l : registry) l.onEvent(e);
    }

    public static void main(String[] args) {
        new AuditListener();
        fire("ready");
    }
}
Output
null: constructing
audit: ready

The first callback reads prefix before the constructor writes it, so it sees the default null, even though the field is final. If registry were read by another thread, that thread would get no final guarantee at all, because the guarantee only holds when this does not escape. The fix is a private constructor and a static factory that registers the object after construction.

Example 3: final field semantics

The JMM gives final fields a special rule (JLS 17.5): a thread that obtains a reference to an object after its constructor finished sees the correctly initialized values of its final fields, and of everything reachable through them as of the end of the constructor, even if the reference was published through a data race.

Java
public class FinalFields {
    static final class Price {
        final String symbol;
        final long ticks;
        Price(String symbol, long ticks) {
            this.symbol = symbol;
            this.ticks = ticks;
        }
    }

    static Price shared;   // still a plain field

    public static void main(String[] args) throws InterruptedException {
        Thread writer = new Thread(() -> shared = new Price("ACME", 10_250));
        writer.start();
        writer.join();
        Price p = shared;
        System.out.println(p.symbol + " " + p.ticks);
    }
}
Output
ACME 10250

Here join() already makes the read safe. The final rule matters when there is no such edge: a racy reader may see shared == null, but if it sees the reference, it cannot see symbol == null or ticks == 0. HotSpot implements this with a StoreStore barrier at the end of a constructor that writes a final field (the "freeze"). The guarantee does not cover non-final fields of the same object, and it does not cover later changes to an object reached through a final field.

Example 4: the safe publication idioms

An object is published safely when the store of the reference and the load by the reader are ordered by happens-before. The standard ways:

Idiom Edge used
Static initializer Class initialization lock (JLS 12.4.2)
volatile field or AtomicReference Volatile write to read
Field guarded by a lock Unlock to lock
Concurrent collection (ConcurrentHashMap, BlockingQueue) The collection's documented edge
final field of a properly constructed object Final field semantics

AtomicReference is a wrapper around a volatile reference with atomic update methods; Atomic Classes and CAS teaches it.

Java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

public class SafeIdioms {
    static class Account {           // mutable, non-final fields
        String id;
        long balance;
        Account(String id, long balance) { this.id = id; this.balance = balance; }
    }

    static final Account STATIC_ACC = new Account("static", 1);           // 1
    static volatile Account volatileAcc;                                  // 2
    static final AtomicReference<Account> atomicAcc = new AtomicReference<>(); // 3
    static final ConcurrentHashMap<String, Account> map = new ConcurrentHashMap<>(); // 4

    public static void main(String[] args) throws InterruptedException {
        Thread writer = new Thread(() -> {
            volatileAcc = new Account("volatile", 2);
            atomicAcc.set(new Account("atomic", 3));
            map.put("m", new Account("map", 4));
        });
        writer.start();
        writer.join();

        long total = STATIC_ACC.balance + volatileAcc.balance
                + atomicAcc.get().balance + map.get("m").balance;
        System.out.println("total = " + total);
    }
}
Output
total = 10

Each idiom covers the state of the object at the moment of publication. Writes made to a mutable object after it was published need their own synchronization.

Example 5: double-checked locking, broken and fixed

Double-checked locking (DCL) checks a field without a lock, and takes the lock only while the field is still null. Without volatile it is broken. The broken version is compiled only; it has no main.

Java
public class BrokenDcl {
    static class Service { int port; Service() { port = 8080; } }

    private static Service instance;            // not volatile: broken

    static Service get() {
        if (instance == null) {                 // racy read
            synchronized (BrokenDcl.class) {
                if (instance == null) {
                    instance = new Service();   // reference may be visible before port = 8080
                }
            }
        }
        return instance;
    }
}

A thread that sees instance != null on the first check never enters the lock, so it has no happens-before edge with the writer. It may see a non-null reference with port == 0. The fix is to declare the field volatile. The local variable avoids a second volatile read on the fast path.

Java
public class FixedDcl {
    static class Service { int port; Service() { port = 8080; } }

    private static volatile Service instance;

    static Service get() {
        Service local = instance;               // one volatile read
        if (local == null) {
            synchronized (FixedDcl.class) {
                local = instance;
                if (local == null) {
                    local = new Service();
                    instance = local;           // volatile write publishes port
                }
            }
        }
        return local;
    }

    public static void main(String[] args) throws InterruptedException {
        Service[] seen = new Service[4];
        Thread[] threads = new Thread[4];
        for (int i = 0; i < 4; i++) {
            int idx = i;
            threads[i] = new Thread(() -> seen[idx] = get());
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        boolean same = seen[0] == seen[1] && seen[1] == seen[2] && seen[2] == seen[3];
        System.out.println("same instance: " + same + ", port " + seen[0].port);
    }
}
Output
same instance: true, port 8080

DCL with volatile has been correct since Java 5 (JSR-133). Making port final would also fix the visibility of port, but the plain read on the fast path would still be racy and is allowed to return null after another thread saw a value.

Example 6: the holder-class idiom

Class initialization is lazy and thread-safe: the JVM initializes a class on first active use, under a per-class lock, and every thread that later uses the class sees the initialized static fields.

Java
public class HolderIdiom {
    static class Registry {
        private Registry() { System.out.println("Registry created"); }

        private static class Holder {
            static final Registry INSTANCE = new Registry();
        }

        static Registry getInstance() { return Holder.INSTANCE; }
    }

    public static void main(String[] args) {
        System.out.println("main started");
        Registry a = Registry.getInstance();
        Registry b = Registry.getInstance();
        System.out.println("same: " + (a == b));
    }
}
Output
main started
Registry created
same: true

Holder is initialized on the first call to getInstance(), not when Registry loads. After initialization the JIT removes the check, so the fast path is a plain static field read. The idiom works only for static singletons; per-instance lazy fields still need DCL with volatile. A single-element enum gives the same laziness and also handles serialization.

Example 7: immutable objects in production code

An immutable object is safe to share through any publication, including a data race, when all of these hold: every field is final, this does not escape during construction, and mutable inputs are copied. Records satisfy the first rule automatically.

Java
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ImmutableBook {
    record Level(long price, long qty) {}

    record BookSnapshot(String symbol, long seq, List<Level> bids, List<Level> asks) {
        BookSnapshot {
            bids = List.copyOf(bids);   // defensive, unmodifiable copies
            asks = List.copyOf(asks);
        }
        long spread() { return asks.get(0).price() - bids.get(0).price(); }
    }

    static final Map<String, BookSnapshot> latest = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        Thread feed = new Thread(() -> {
            var bids = new java.util.ArrayList<>(List.of(new Level(10_000, 5)));
            var asks = new java.util.ArrayList<>(List.of(new Level(10_002, 3)));
            latest.put("ACME", new BookSnapshot("ACME", 1, bids, asks));
            bids.clear();               // does not affect the snapshot
        });
        feed.start();
        feed.join();

        BookSnapshot s = latest.get("ACME");
        System.out.println(s.symbol() + " seq=" + s.seq() + " bids=" + s.bids().size()
                + " spread=" + s.spread());
    }
}
Output
ACME seq=1 bids=1 spread=2

The feed thread mutates its own list after publishing, and the snapshot is unaffected because the compact constructor copied it. Readers on any number of threads can hold the snapshot without locks. A new market state is a new snapshot, not a change to the old one.

Memory barriers and hardware

A memory barrier (fence) forbids certain reorderings of loads and stores around it. The classic names describe which pair stays ordered:

Barrier Keeps ordered
LoadLoad Earlier loads before later loads
StoreStore Earlier stores before later stores
LoadStore Earlier loads before later stores
StoreLoad Earlier stores before later loads (the most expensive)

The JMM is specified in terms of happens-before, not barriers. The barrier view is how JIT compilers implement it. In the JSR-133 cookbook mapping, a volatile store is preceded by StoreStore and LoadStore barriers and followed by a StoreLoad barrier; a volatile load is followed by LoadLoad and LoadStore barriers.

CPU Hardware reorders What a volatile store costs
x86-64 (TSO) Only store followed by load (store buffer) LoadLoad, LoadStore, StoreStore are free; StoreLoad needs a locked instruction; HotSpot emits a lock addl to a stack slot after the store
ARMv8 (AArch64) All four kinds HotSpot compiles a volatile load to ldar (load-acquire) and a volatile store to stlr (store-release)

On x86 a volatile load compiles to a plain mov; only the compiler is restricted. This is why code with missing volatile often passes on x86 and fails on ARM: the hardware already preserves most orders, and only compiler reordering exposes the bug.

Interview points

  • Publication is safe only through an action with a happens-before edge: static init, volatile, AtomicReference, a lock, a concurrent collection, or final fields.
  • final fields are visible after construction even through a racy reference, provided this did not escape from the constructor.
  • Starting a thread or registering a listener in a constructor lets this escape; use a static factory.
  • DCL needs a volatile field; without it a reader can see a reference to a partly constructed object.
  • The holder-class idiom gives lazy, lock-free static singletons through class initialization.
  • x86 only reorders store-then-load, so only StoreLoad costs an instruction; ARM needs acquire and release instructions for volatile accesses.

Summary

  • Unsafe publication can show default field values in another thread, and a plain-field spin loop may never end.
  • final semantics are implemented with a StoreStore "freeze" at the end of the constructor.
  • Immutable objects need all fields final, no this escape, and defensive copies of mutable inputs.
  • Safe publication covers the state at publication time; later mutations need their own synchronization.
  • The four barrier types are LoadLoad, StoreStore, LoadStore, and StoreLoad; StoreLoad is the expensive one.