4

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?

  • 1
    Your understanding was true before Java 5. And it still would be true when the field is modified after publication. – Holger Feb 22 '22 at 12:27

3 Answers3

7

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.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
4

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

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 1
    The happens-before order is the transitive closure of the union of the synchronizes-with order and the program order. So if you have established that 2 actions are ordered by the synchronizes-with order, it is automatically implied that they are ordered by the happens-before order. – pveentjer Feb 24 '22 at 05:33
  • 1
    In other words, the fact that the volatile write and read (to the same variable) have a “synchronizes-with” relation is the *prerequisite* to have a *happens-before* relationship. As §17.4.5. says at the beginning “*If an action x synchronizes-with a following action y, then we also have hb(x, y).*” It than proceeds to say “**It follows from the above definitions that:** *A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.*” This conclusion is what we want; since it has been spelled out explicitly, there is no need to discuss “synchronizes-with” at all. – Holger Feb 24 '22 at 10:07
  • @Holger you're right, I've edited. – Eugene Mar 02 '22 at 15:09
  • Small correction: Happens-before consistent is a relaxation of happens-before. Happens-before consistent also deals when a read and write are concurrent (so not ordered by happens-before; so in data race). – pveentjer Mar 02 '22 at 17:17
  • @pveentjer right, but isn't a data race a result of a missing happens before chain? I mean the phrase: "... are not ordered by a happens-before relationship, it is said to contain a data race" from chapter 17 of JLS. My point being that, unless there is HB for all actions, yes, a data race can happen. Was that your point? thank you. – Eugene Mar 02 '22 at 19:13
3

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.