Java Constructor And Thread-Safety

Java constructor and thread-safety

1. What a constructor call does mechanically

Calling a constructor like new Foo(args) roughly does three things, in order, within the calling thread:
  1. Allocate memory for the object, with all fields set to their default values (0, false, null).
  2. Run field initializers, instance initializer blocks, and the constructor body, in textual order (superclass constructor first).
  3. Return a reference to the object.

From the calling thread's point of view, this is just ordinary sequential program order — nothing surprising.

2. The core issue: no happens-before by default

The Java Memory Model (JMM) only guarantees cross-thread visibility along happens-before edges — things like a volatile write before a later volatile read, a monitor unlock before a later lock, or Thread.start() before the new thread's actions. A constructor call, by itself, creates no such edge.

That means if thread A does:

    Foo f = new Foo(); // sets f.x = 42 in the constructor
    sharedRef = f; // publish via a plain (non-volatile) field

and thread B later reads sharedRef, there is no guarantee that B sees x == 42.

Thread B might see the reference sharedRef correctly, and yet still see x == 0 (the default value), because the compiler/JIT/CPU are free to reorder the write to x relative to the write of sharedRef, from Thread B's perspective, in the absence of any synchronization action.

This is the classic unsafe publication hazard, and it's the reason things like non-volatile double-checked locking are broken.

Conclusion: Calling a constructor gives you no happens-before guarantee to other threads unless you also use some synchronization mechanism to publish the reference such as one of the following:

  1. synchronized
  2. volatile
  3. Thread.start()
  4. a concurrent collection's own guarantees, java.util.concurrent locks, etc.

2.1 The one exception: final fields

JSR-133 (the JMM introduced in Java 5) carved out a special rule for final fields specifically because unsafe publication was so common and painful.

The guarantee is:

  If an object is properly constructed (meaning "this" does not escape during 
  construction), then any thread that obtains a reference to that object
  after the constructor has finished is guaranteed to see the correctly
  initialized values of its final fields — even without synchronization.

Mechanically, the JVM inserts a "freeze" (a store barrier) at the end of the constructor for each final field, which prevents the write to that field from being reordered past the point where the reference becomes visible to other threads.

2.2 Important caveats:

  • This guarantee applies only to final fields, not to ordinary fields, even if they're set in the constructor and never changed afterward.
  • If the final field itself holds a reference to a mutable object, the guarantee only covers seeing the reference correctly — not necessarily the internal state of that referenced object (though in practice it usually works out because that object's own construction also completed before this constructor did, along program order — but relying on transitive guarantees like this is risky and worth being explicit about).

2.3 The other big caveat: this escaping

The final-field guarantee assumes this doesn't escape during construction. It escapes if, from inside the constructor, you:

  • start a thread, passing this or this's methods
  • register this as a listener/callback
  • call an overridable instance method (which could be overridden to leak this)
  • store this into a static field or shared collection

Note: If any of this happens, another thread may observe the object before construction even finishes, in which case even final fields may appear as their default values. The JMM's guarantee is void in this case — it's entirely on you to avoid it.

Comments

Popular posts from this blog

Recent Innovations in Ergonomic Split Designs

A Smarter Way to Type: Ergonomic Mechanical Keyboards

Traveling on a Budget: Tips for Affordable Adventures