There are two insights that may help you here. First, a constructor is (mostly) just like any other method, for purposes of synchronization; namely, it doesn't inherently provide any (except as noted below). And secondly, thread safety is always between individual actions.
So let's say you have the following constructor:
MyClass() {
this.i = 123;
MyClass.someStaticInstance = this; // publish the object
}
// and then somewhere else:
int value = MyClass.someStaticInstance.i;
The question is: what can that last expression do?
- It can throw an NullPointerException, if
someStaticInstance
hasn't been set yet.
- It can also result in
value == 123
- But interestingly, it can also result in
value == 0
The reason for that last bit is that the actions can be reordered, and a constructor isn't special in that regard. Let's take a closer look at the actions involved:
- A. allocating the space for the new instance, and setting all its fields to their default values (0 for the
int i
)
- B. setting
<instance>.i = 123
- C. setting
someStaticInstance = <instance>
- D. reading
someStaticInstance
, and then its i
If you reorder those a bit, you can get:
- A. allocating the space for the new instance, and setting all its fields to their default values (0 for the
int i
)
- C. setting
someStaticInstance = <instance>
- D. reading
someStaticInstance
, and then its i
- B. setting
<instance>.i = 123
And there you have it -- value
is 0, instead of 123.
JCIP is also warning you that leaks can happen in subtle ways. For instance, let's say you don't explicitly set that someStaticInstance
field, but instead merely call someListener.register(this)
. You've still leaked the reference, and you should assume that the listener you're registering with might do something dangerous with it, like assigning it to someStaticInstance
.
This is true even if that i
field is final. You get some thread safety from final fields, but only if you don't leak this
from the constructor. Specifically, in JLS 17.5, it says:
An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.
The only way to guarantee the "only see a reference to an object after that object has completely initialized" is to not leak the reference from its constructor. Otherwise, you could imagine a thread reading MyClass.someStaticInstance
in the moment just after the field was set, but before the JVM recognizes the constructor as finished.