skip to content

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%

answer

  1. guarantee covers references obtained after the freeze
  2. `new Thread(this).start()` in a constructor
  3. listener/registry registration inside constructor
  4. lambdas and inner classes capture `this`
  5. construct, then publish via static factory

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.

solid answer

~50 s

The frozen-field guarantee is stated over references obtained **after** the freeze that happens at the end of the constructor. A constructor that publishes `this` — by starting a thread that captures it, registering itself as a listener, adding itself to a static registry, or calling an overridable method that does any of those — hands another thread a reference that predates the freeze. That reference is outside the guarantee, so the other thread may read final fields at `0` or `null`, and may even see them change value later. A non-static inner class, an anonymous class, or a lambda created in the constructor and handed to something that stores or runs it is the usual accidental leak, because it captures `this` implicitly. The fix is to keep construction private: assign fields only, then publish in a static factory method after `new` returns, or expose a separate `start()`/`register()` call the caller invokes afterwards.

code

java · 22 lines
java
// BROKEN: the listener is reachable before the freeze
final class Leaky {
    final int id;
    Leaky(EventBus bus, int id) {
        bus.subscribe(e -> handle(e));  // captures `this`, escapes now
        this.id = id;                   // another thread may read id == 0
    }
    void handle(Event e) { /* uses id */ }
}

// SAFE: freeze happens before anyone else can reach the object
final class Safe {
    final int id;
    private Safe(int id) { this.id = id; }

    static Safe register(EventBus bus, int id) {
        Safe s = new Safe(id);          // constructor complete -> fields frozen
        bus.subscribe(s::handle);       // publish afterwards
        return s;
    }
    void handle(Event e) { /* uses id */ }
}

go deeper

for a junior

Know the rule of thumb: do not give the object away inside its constructor — no starting threads, no registering listeners, no passing this anywhere.

for a middle

Explain that the frozen-field guarantee only covers references obtained after the constructor finishes, and name the common leak shapes including implicit this capture by lambdas and inner classes.

for a senior

Add the subclass-ordering trap with overridable calls, describe the observable symptom (nullable final field, warm-up-dependent behaviour), and give the construct-then-publish factory as the fix.

for a principal

Treat it as a class-design invariant: constructors assign, factories publish, self-registration is an explicit lifecycle call; and note that a self-publishing class cannot be safely subclassed, which is a documented API constraint, not a coding nit.

## Why `final` is not enough on its own The memory model grants final fields their guarantee through a *freeze* action placed at the end of the constructor, and states the guarantee only for a thread that obtains the object's reference through a chain of reads beginning after that freeze. Nothing is promised about a reference that existed **before** the freeze. So the guarantee has one precondition the compiler does not check for you: the object under construction must not become reachable from another thread until the constructor completes. ## What an escape looks like Several shapes leak `this`, and most do not contain the word `this` at all: - **Starting a thread in the constructor.** `new Thread(this).start()` — or a lambda that touches an instance field — gives the new thread a live reference immediately. Note that `Thread.start()` does create an ordering edge for everything written *before* the call, so fields assigned earlier in the constructor are visible; the danger is fields assigned *after* the `start()` line, and any subclass constructor body that has not run yet. - **Registering a listener or callback.** `bus.subscribe(this)`, `registry.put(key, this)`, `parent.addChild(this)`. If the collection is visible to other threads, the object is published half-built. - **Implicit captures.** An anonymous inner class, a non-static inner class instance, or a method reference such as `this::handle` created inside the constructor and stored somewhere carries the enclosing `this` with it. - **Calling an overridable method.** A constructor calling a non-final, non-private method may dispatch into a subclass override that runs *before* the subclass constructor body, sees the subclass's own final fields at defaults, and may itself leak `this`. ## What the other thread can observe Once the reference escaped, the reading thread is in ordinary racy-read territory for every field, final or not. It may see the default value, it may see the assigned value, and — the part that surprises people — it may see the field with one value and then with another on a later read, because there was never a freeze in its ordering. Code written on the assumption "final fields never change" can therefore observe them changing. Optimizers are permitted to fold repeated reads of a final field into one, which makes the resulting behaviour inconsistent between interpreted, C1-compiled and C2-compiled runs of the same method — a classic "only breaks in production after warm-up" bug. ## The subclass trap Even a leak-free superclass constructor can be undermined by inheritance. Construction runs superclass constructor first, then subclass field initializers and body. If the superclass constructor publishes the object, the subclass's final fields are certainly not frozen yet — they have not even been assigned. This is why a class that publishes itself during construction is fundamentally unsafe to subclass, and why documentation for such classes tends to say "do not extend". ## How to avoid it The standard remedy is to separate construction from publication: - Do only field assignment in the constructor. Nothing that hands the instance to anyone. - Use a **static factory**: construct into a local variable, then publish. The `new` expression's result is only reachable by the constructing thread until the factory returns it, so the freeze precedes publication. - If a class must register itself or start a thread, expose an explicit `start()` or `register()` method that the caller invokes after the constructor returns, and document it. - Make the class `final`, or make any method the constructor calls `private` or `final`, so no override can run before the subclass is initialized. ## How you would detect it Static analysis catches the common shapes — most linters have a rule for constructors that pass `this` out or call overridable methods. Beyond that, the symptom in review is the giveaway: a field that is `final` yet is sometimes null in a stack trace, or an object that behaves as if it were half-built only under load, on a weakly ordered CPU, or after the JIT has warmed up.

  • Why is calling a non-final instance method from a constructor dangerous even when nothing is obviously published?
    Java runs the superclass constructor before the subclass field initializers and constructor body. A call to an overridable method from the superclass constructor dispatches to the subclass override, which then executes against a subclass whose fields — including final ones — are still at defaults. The override can also hand `this` to something else, converting a subtle initialization bug into a full escape.
  • Does `new Thread(this).start()` inside a constructor break visibility of fields assigned before that line?
    No. Starting a thread creates a happens-before edge, so everything the constructing thread wrote before `start()` is visible to the new thread. The problems are fields assigned after that line, any subclass constructor body that has not run, and the fact that the reference has now escaped to other paths where no such edge exists.

saying these in an interview costs you the question

  • Believing `final` alone makes the guarantee unconditional, regardless of what the constructor does.
  • Not recognizing that an anonymous class, inner class, or lambda in a constructor captures `this`.
  • Thinking a final field can never be observed with two different values by the same thread.
  • Claiming the problem only exists on weakly ordered CPUs, when the JIT can reorder on any platform.
  • Proposing to fix the escape by adding `volatile` to the field instead of removing the escape.

context