skip to content

What is safe publication, and why can sharing an object across threads be broken even when the object itself is correct?

level: seniorimportance: must knowfreq 60%

answer

  1. non-null reference can still show default fields
  2. reordering + caching break naive publication
  3. happens-before edge = fully-constructed visible
  4. four idioms: static init / volatile|Atomic / lock / final
  5. immutable = safe even through a data race

basics

~20 s

Safe publication means handing an object to another thread in a way that guarantees that thread sees it fully built, not half-initialized. Without it, the receiving thread might see a null or stale fields due to reordering and CPU caches, even if the object is correct.

solid answer

~50 s

When you make an object visible to another thread (publish it), the Java Memory Model does not by default guarantee the other thread sees the object's fields fully and correctly initialized. The publishing write and the constructor writes can be reordered, and CPU caches mean another thread may see a stale or partial object, even a non-null reference pointing at fields that still read their default values. Safe publication establishes a happens-before edge so the receiver sees a fully-constructed object. The standard safe-publication idioms are: initialize the reference in a static initializer; store it in a `volatile` or `AtomicReference`; store it via a properly locked (synchronized) section; or store it into a final field of a correctly constructed object. Immutable objects with all-final fields are a special case: they are safe to publish even through a data race, thanks to final-field semantics. Mutable objects must use one of the proper handoff mechanisms.

code

java · 16 lines
java
// Unsafe publication: another thread may see a non-null holder with default fields
class UnsafeHolder { Object ref; }            // plain field

// Safe publication of a mutable object via volatile
class SafeHolder {
    private volatile Resource resource;        // volatile = happens-before edge
    void publish(Resource r) { this.resource = r; }
    Resource get() { return resource; }        // reader sees a fully built r
}

// Immutable objects need no special mechanism
final class Config {
    private final int port;                     // final-field semantics
    Config(int port) { this.port = port; }
    int port() { return port; }
}

go deeper

for a junior

Recognizes the term: handing an object to another thread can be unsafe if the other thread sees a half-built object; knows immutable objects sidestep the issue.

for a middle

Can name the safe-publication idioms (volatile/Atomic, synchronized, static init, final field, concurrent collections) and use one correctly.

for a senior

Explains happens-before and reordering/visibility, the partially-constructed-object hazard, why immutable objects are safe through a data race, and the double-checked-locking fix.

for a principal

Reasons about JMM guarantees across a system, chooses publication strategy per data type, defines effectively-immutable conventions, and reviews code for unsafe-publication and this-escape bugs.

## Setting the scene: what 'publication' is **Publishing** an object means making its reference reachable by code outside the scope that created it — for example, assigning it to a field that another thread reads, putting it in a shared collection, or returning it from a factory. **Safe publication** is publication done so that the receiving thread is guaranteed to see the object in a fully, correctly constructed state. ## Why naive publication is broken You might think: 'I construct the object, then I assign it to a shared field; surely another thread that reads a non-null reference sees a finished object.' On real hardware and under the **Java Memory Model (JMM)** — the rules defining when one thread's writes become visible to another — that is **not** guaranteed, for two reasons: 1. **Reordering.** The compiler and CPU may reorder independent writes. The write that publishes the reference can become visible *before* the writes that initialize the object's fields. 2. **Visibility / caching.** Without a memory barrier, a thread may read cached values. It can see the new reference but stale (default) field values: `0`, `false`, `null`. The scary outcome: another thread reads a **non-null** reference but observes its fields with their default values, as if the constructor hadn't run. This is the classic unsafe-publication bug behind the broken double-checked locking idiom (pre-`volatile`). ## Happens-before: the rule that fixes it The JMM defines a **happens-before** relationship: if action A happens-before action B, then B is guaranteed to see everything A did. Safe publication works by creating a happens-before edge between 'finished constructing the object' (in the publisher) and 'read the reference' (in the consumer). Once that edge exists, the consumer must see the fully-initialized object. ## The four safe-publication idioms From *Java Concurrency in Practice*, an object is safely published if you do any of: 1. **Static initializer.** Initialize the reference from a `static { }` block or static field initializer. Class initialization is synchronized by the JVM. 2. **`volatile` field or `AtomicReference`.** Store the reference in a `volatile` field (or an `AtomicReference`/`AtomicReferenceFieldUpdater`). A write to a volatile happens-before any subsequent read of it. 3. **Lock-guarded field.** Store and read the reference inside the **same lock** (a `synchronized` block or `Lock`). Unlocking happens-before a later lock of the same monitor. 4. **`final` field.** Store the object into a `final` field of an object that is itself correctly constructed (its `this` doesn't escape). Final-field semantics guarantee the values are visible. Thread-safe collections do this for you: putting an object into a `ConcurrentHashMap`, `BlockingQueue`, `Collections.synchronizedMap`, etc., safely publishes it to threads that retrieve it. ## The immutability shortcut An **immutable** object — all fields `final`, no escape during construction, no mutation afterward — is **safe to publish through any mechanism, including a plain non-volatile field, even via a data race**. This is the strongest reason to prefer immutability: you get correct sharing without any synchronization at the handoff. (A mutable object is *not* covered by this and must use idioms 1–3 or 4-with-care.) ## Effectively immutable objects An object that is technically mutable but is never modified after publication is **effectively immutable**. If it is *safely* published and then treated as read-only, it is also safe to share. The discipline (never mutate after publishing) is yours to enforce. ## Putting it together with the leaf's strategies - **Immutability**: makes shared objects safe by forbidding change (and gives free safe publication). - **Confinement**: avoids sharing entirely, so publication never happens. - **Safe publication**: the mechanism you need precisely when sharing *is* unavoidable and the object is mutable — it's the bridge that hands a correctly built object from one thread to another.

  • Why was the original (pre-volatile) double-checked locking idiom broken?
    The instance reference could be published before the constructor's field writes were visible, so a second thread could read a non-null reference to a partially constructed object. Marking the field volatile (Java 5+) adds the needed happens-before edge and fixes it.
  • Do you need safe publication for an immutable object?
    No special mechanism is required: an object with all-final fields and no escape during construction is safe to publish through any means, including a plain field, even via a data race. That's a key benefit of immutability.

saying these in an interview costs you the question

  • Assuming 'reference is non-null, so the object must be fully built'
  • Treating any field assignment as a safe handoff without volatile/lock/final
  • Thinking volatile makes the whole object thread-safe rather than just safely publishing the reference
  • Calling an object safely published when it's mutated after publication via a non-synchronized path

context