skip to content

Immutable/Frozen Field Semantics

The guarantee that fields frozen at the end of construction are visible to any thread that gets the reference, with no extra synchronization — provided the reference does not escape during construction. Interviewers probe the boundary: the guarantee covers the reference itself, not concurrent access to whatever mutable object it points at.

on this pageshow

questions

4

In the Java Memory Model, what special visibility guarantee applies to an instance field declared `final`, and what must the reading thread do to obtain it?

level: middleimportance: must knowfreq 55%

answer

  1. freeze at end of constructor
  2. reader needs no synchronization
  3. dereference chain also frozen
  4. `this` must not escape
  5. publication, not mutation, guarantee

basics

~20 s

Final fields are frozen when the constructor returns. Any thread that later obtains the object's reference sees those fields correctly initialized, with no lock and no volatile on the reader's side, provided the reference did not escape during construction.

solid answer

~50 s

The memory model defines a **freeze action** on every final field at the end of the constructor. A thread that obtains a reference to the object *after* construction completed is guaranteed to see the frozen values, and to see versions of anything reachable through those final fields that are at least as up to date as the final fields were at freeze time. This is the only ordering guarantee in the model that demands **nothing from the reader**: no `synchronized`, no `volatile` read, no atomic. It is why `String` or a class with only final fields can be handed to another thread through a plain field or a plain `HashMap` and still be observed fully built. The condition sits on the writer side: the object must not publish `this` before the constructor finishes. If another thread can reach the object early, it may observe the final fields at default values (`0`, `null`), and the guarantee is void.

code

java · 16 lines
java
final class Holder {
    final int guaranteed;   // frozen at end of constructor
    int plain;              // no guarantee for other threads

    Holder(int v) {
        this.guaranteed = v;
        this.plain = v;
    }
}

// Thread A: shared = new Holder(42);          // plain, racy write of the reference
// Thread B: Holder h = shared;
//           if (h != null) {
//               h.guaranteed  // always 42
//               h.plain       // may legally be 0
//           }

go deeper

for a junior

Recall the headline: fields marked final are set once in the constructor and other threads that get the object afterwards see them properly set, without extra synchronization.

for a middle

State the freeze-at-end-of-constructor rule, that the reader needs no synchronization, and the this-must-not-escape condition. Contrast with a non-final field in the same class.

for a senior

Add the dereference-chain part (state reachable through a final field is covered), the racy-publication case, and the boundary where the guarantee stops at mutable contents or post-construction reflective writes.

for a principal

Frame it as the one asymmetric, reader-free guarantee in the model, explain what it buys design-wise (immutable objects shareable through any channel), and name the invariants a codebase must hold to keep it — no this escape, no post-construction mutation, no exposed mutable internals.

## What goes wrong without it Ordinary field writes carry no cross-thread ordering. If thread A builds an object and stores its reference into a plain, non-volatile field, thread B reading that field may legally see the reference but observe the object's fields at their **default values** (`0`, `false`, `null`). Both the JIT and the hardware are free to make the store of the reference visible before the stores that filled in the fields, because within thread A nothing depends on the order. The memory model carves out one exception to that rule, and it applies to fields declared `final`. ## Freeze actions The specification introduces an action that exists nowhere in the source code: at the **end of the constructor** in which a final field is set, a *freeze* of that field occurs. The ordering rule is then stated over the freeze rather than over the assignment: if a thread obtains a reference to the object only through a chain of reads that begins after the freeze, that thread is guaranteed to read the frozen value, not the default value. Two details of that phrasing matter. 1. **The reader does nothing.** Every other guarantee in the model is a pair: a release on one side, an acquire on the other. Frozen fields are one-sided. The publishing thread pays a small cost at the end of the constructor; the reading thread pays nothing, holds no lock, and does not have to read anything `volatile`. Even a *racy* publication — a plain write of the reference that another thread happens to read — still delivers the correct final-field values. 2. **The guarantee extends one step outward.** The spec says a reader also sees versions of any object or array referenced by a final field that are **at least as up to date** as the final field was when frozen. So a final field pointing at an array whose elements were filled before the constructor ended gives you the filled elements, not a zeroed array. This is often called the *dereference chain* or deep-freeze part of the rule, and it is what makes `String` — whose character array is written during construction — safe to share. ## The condition: no early publication of `this` The guarantee is stated over references *obtained after* the constructor completes. If the constructor leaks `this` — registers a listener, starts a thread, stores itself in a static registry, calls an overridable method that leaks it — another thread can hold the reference before the freeze happens. That reference was not obtained after the freeze, so the model gives it nothing: the other thread may read `0` or `null` from a field the constructor already assigned, and may even see the field change value under it. ## Where the guarantee stops It guarantees the *value of the field*. If that value is a reference to a mutable object, only the reference and the state it had at freeze time are covered; later mutations by other threads are ordinary racy writes with no guarantee at all. Frozen fields give you safe **publication**, not thread safety of whatever you published. The rule also assumes the field is genuinely never modified after construction. Deserialization frameworks and reflection can write final fields after the freeze; the spec explicitly says that a thread which read the field before such a modification is not required to observe the change, and the guarantee becomes murky. Treat post-construction mutation of a final field as being outside the model. ## Practical consequence A class whose fields are all final, whose constructor does not leak `this`, and which does not expose mutable internals can be published across threads by any means at all — a plain field, a non-thread-safe map, a data race — and every reader that sees the reference sees a fully built object. That property is a feature of the *specification*, not of any particular collector or CPU, so it holds on every conforming JVM.

  • Does the guarantee still hold if the reference itself is published through a data race, for example a plain non-volatile static field?
    Yes. That is precisely what makes the rule special. The guarantee is written over the freeze and the reference-obtaining read, not over any synchronization between the threads. As long as the reference was written after the constructor completed and the reader gets it from that write, the final fields read correctly — even though the reader may or may not see the reference at all, and non-final fields of the same object are still unguaranteed.
  • An object has one final field pointing to an `int[]` filled inside the constructor. What does a reader see?
    It sees the array reference and elements at least as up to date as they were when the field was frozen, so the values written in the constructor are visible. The guarantee covers the state reachable through the final field at freeze time. Any element written to that array *after* the constructor returned is an ordinary racy write with no guarantee.
  • What breaks the guarantee even when every field is declared final?
    Publishing `this` before the constructor completes. Starting a thread that captures `this`, registering a callback, or storing the instance in a static registry from inside the constructor all let another thread obtain the reference before the freeze, so that reference is not covered and the fields may be observed at default values.

A freeze is like sealing a shipping crate at the loading dock: whoever receives the crate afterwards is guaranteed to find everything that was inside when it was sealed, without having to coordinate with the packer. If you hand out the crate's address before you seal it, that promise is off.

saying these in an interview costs you the question

  • Claiming `final` makes the object or its contents immutable or thread-safe, rather than guaranteeing publication of the field value.
  • Saying the reader must still use `volatile` or a lock to see final fields.
  • Believing the guarantee holds for non-final fields set in the same constructor.
  • Thinking the guarantee survives a constructor that leaks `this`.
  • Asserting `final` inserts a lock or makes reads more expensive for the reader.

context

open as a page

A class assigns all of its fields in its constructor and the fields are declared `final`, yet another thread occasionally observes those fields at zero or null. Explain how that is possible and what in the constructor causes it.

level: seniorimportance: must knowfreq 45%

basics

~20 s

The constructor let this escape — it published the object (started a thread, registered a callback, stored itself somewhere) before the constructor finished. The frozen-field guarantee only covers references obtained after construction completes, so an early reference sees defaults.

open as a page

A field is declared `final` and holds a reference to a `HashMap` that is later modified by several threads. What exactly does the memory model guarantee here, and what does it not?

level: middleimportance: should knowfreq 42%

basics

~20 s

It guarantees the reference itself, and the map contents as of the end of the constructor, are visible to any thread that obtains the object. It guarantees nothing about entries added or changed afterwards — those are unsynchronized races.

open as a page

What does a JVM actually have to emit at the end of a constructor to deliver the frozen-field guarantee, and why do reading threads need nothing on their side?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

The JVM must keep the final-field stores from moving after the object reference becomes visible. HotSpot emits a store-store barrier at constructor end — free on x86's strongly ordered stores, a real instruction on ARM. Readers need nothing because address dependencies preserve the load order.

open as a page