Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concurrent access to a public field. Why is it possible to observe inconsistent state?

I'm reading B. Goetz Java Concurrency In practice and now I'm at the section 3.5 about safe publication. He stated:

// Unsafe publication
public Holder holder;
public void initialize() {
    holder = new Holder(42);
}

This improper publication could allow another thread to observe a partially constructed object.

I don't see why it is possible to observe a partially constructed subobject. Assume, that the constructor Holder(int) does not allow this to escape. So, the constructed reference can be observed only by the caller. Now, as JLS 17.7 stated:

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.

it is impossible for thread to observe a partially constructed object.

Where was I wrong?

like image 951
St.Antario Avatar asked Dec 30 '15 07:12

St.Antario


Video Answer


1 Answers

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)?

  1. the JVM allocates some space for the new Holder object, with default values for all its fields (ie, value = 0)
  2. the JVM invokes the Holder constructor
    • the JVM sets <new instance>.value to the incoming value (42).
    • the constructor completes
  3. 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.

like image 150
yshavit Avatar answered Nov 16 '22 01:11

yshavit