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 someStaticInstancehasn'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 itsi
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 itsi
- 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.