Can another thread see an effectively immutable object in an inconsistent state if it is published with a volatile reference?

Viewed 119

According to Java Concurrency in Action if we have the following class:

public class Wrapper {
  private int num;

  public Wrapper(int num) {
    this.num = num;
  }

  public void assertCorrectness() {
    if (num != num)
      throw new AssertionError("This is false");
  }
}

and we initialise an instance of this class and publish it in a non-safe way (through a simple public field for example), then the assertCorrectness() might indeed throw an AssertionError, if called from another thread. In other words, this means that some another thread might see an up-to-date reference to the instance, but the state of the instance itself could be out-of-date (so a thread can see an object exists but it is in a partially constructed / inconsistent state).

On the other hand, it is said that publishing an instance of this class through a volatile reference is considered safe. However it was my understanding that volatile just guarantees that any thread will always see an up-to-date version of a reference, but not the object's state being referenced. So we can be sure that if one thread assigns a new instance of the Wrapper class to a volatile field, then all the other threads will see that the reference was updated. But is there a risk that they will still see an object in an inconsistent / partially constructed state?

3 Answers

No, because volatile being used establishes a happens-before relationship. Without it various reorderings and other things are allowed, which make the inconsistent state possible, but with it the JVM must give you the expected outcome.

In this case volatile is not used for the visibility effects (threads seeing up to date values), but the safe publishing provided by the happpens-before. This feature of volatile is often left out when its use is explained.

The accepted answer is simple, yet great. volatile is rarely explained and used for safe publishing, yet it provides the needed guarantees.

imho, to properly prove that this works with volatile, there are some things that need explaining.

The first one is "program order", or the perceivable order in which things happen within a thread. We can draw this to easier grasp things:

 --------------- T1 -------------
 | write to num or this.num=num |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T1 -------------
 |    write Wrapper instance    |
 --------------------------------
                 |
                \|/  (??)
 --------------- T2 -------------
 |    read Wrapper instance     |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T2 -------------
 |      read num first time     |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T2 -------------
 |      read num second time    |
 --------------------------------

T1 and T2 are thread1 and thread2 and PO is program order. Now a rule in the JLS says this:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

As such we can replace PO in the above drawing with HB (happens-before).

At the same time, if wrapper instance is not volatile, there is no special relation between writing to wrapper (from T1) and reading from wrapper in T2.

We need to also introduce here happen-before consistency:

a read sees the last write in happens-before order, or any other write.

Since we do not have a full chain of happens-before here (that ?? does not establish one), we get that : "...or any other read", which means we can read different values in those two reads of num (in that num != num).


If you make wrapper instance volatile, JLS says that:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

As such, we have this now:

 --------------- T1 -------------
 | write to num or this.num=num |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T1 -------------
 |    write Wrapper instance    |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |    read Wrapper instance     |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |      read num first time     |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |      read num second time    |
 --------------------------------

Now, happens-before consistency says : "a read sees the last write in happens-before order..."; the key part here is "in happens-before order". This means that both reads of num will see the write of it (again: in happens before order).

The answer above is correct.

Just keep in mind that effectively immutable + safe publication behaves unintuitively in some cases.
For instance:

  1. If

    • at first thread 1 safely publishes object o to thread 2
    • then thread 2 unsafely publishes object o to thread 3

    in the end thread 3 can see object o in an inconsistent state
    See [1] and [2]

  2. also this

Real immutable objects have no such problems.

Related