Java Constructor And Thread-Safety
Java constructor and thread-safety
1. What a constructor call does mechanically
Calling a constructor likenew Foo(args) roughly does three things,
in order, within the calling thread:
- Allocate memory for the object, with all fields set to their default values (0, false, null).
- Run field initializers, instance initializer blocks, and the constructor body, in textual order (superclass constructor first).
- 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:
synchronizedvolatile-
Thread.start() -
a concurrent collection's own guarantees,
java.util.concurrentlocks, 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
finalfields, not to ordinary fields, even if they're set in the constructor and never changed afterward. -
If the
finalfield 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
thisorthis's methods - register
thisas a listener/callback -
call an overridable instance method (which could be overridden to leak
this) - store this into a
staticfield 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
Post a Comment