So, the constructed reference can be observed only by the caller.
That's where your logic breaks, though it seems like a perfectly reasonable thing to say.
First things first: The atomicity that 17.7 mentions only says that when you read a reference, you'll either see all of a previous value (starting with its default value of null
) or all of some subsequent value. You'll never get a reference with some bits corresponding to value 1 and some bits corresponding to value 2, which would essentially make it a reference into a random place in the JVM heap — which would be terrible! Basically they're saying, "the reference itself will either be null, or point to a valid place in memory." But what's in that memory, that's where things can get weird.
Setting up a simple example
I'll assume this simple Holder:
public class Holder {
int value; // NOT final!
public Holder(int value) { this.value = value; }
}
Given that, what happens when you do holder = new Holder(42)
?
- the JVM allocates some space for the new Holder object, with default values for all its fields (ie,
value = 0
)
- the JVM invokes the Holder constructor
- the JVM sets
<new instance>.value
to the incoming value (42).
- the constructor completes
- the JVM returns the reference to that object we just allocated, and sets
Holder.holder
to this new reference
Reordering makes life hard (but it also makes programs fast!)
The problem is that another thread can view these events in any order, since there are no synchronization points between them. That's because constructors don't have any special synchronization or happens-before semantics (that's slight lie, but more on that later). You can see the full list of "synchronized-with" actions at JLS 17.4.4; note that there's nothing there about constructors.
So, another thread might see those actions ordered as (1, 3, 2). This means that if some other event is ordered between events 1 and 3 — for instance, if someone reads Holder.holder.value
into a local var — then they'll see that newly allocated object, but with its values before the constructor has run: you'd see Holder.holder.value == 0
. This is called a partially constructed object, and it can be pretty confusing.
If the constructor had multiple steps (setting multiple fields, or setting and then changing a field), then you can see any ordering of those steps. Pretty much all bets are off. Yikes!
Constructors and final
fields
I mentioned above that I lied when I asserted that constructors don't have any special synchronization semantics. Assuming you don't leak this
, there's one exception to that: any final
fields are guaranteed to be seen as they were at the end of the constructor (see JLS 17.5).
You can think of it as there being a kind of synchronization point between steps 2 and 3, but it only applies to final
fields.
- It doesn't apply to non-final fields
- It doesn't apply transitively to other synchronization points.
- It does, however, extend to any state that you access through the
final
fields. So, if you have a final List<String>
, and your constructor initializes it and then adds some values, then all threads are guaranteed to see that list with at least the state that it had at the end of the constructor, including those add
calls. (If you modify the list after the constructor, without synchronization, then all bets are off again.)
That's why it was important in my example above that value
was not final. If it had been, then you wouldn't be able to see Holder.holder.value == 0
.