Is it guaranteed that volatile field would be properly initialized

Viewed 266

Here:

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.

Are the same guarantees held for the volatile field? What if the y field in the following example would be volatile could we observe 0?

class FinalFieldExample { 
final int x;
int y; 
static FinalFieldExample f;

public FinalFieldExample() {
    x = 3; 
    y = 4; 
} 

static void writer() {
    f = new FinalFieldExample();
} 

static void reader() {
    if (f != null) {
        int i = f.x;  // guaranteed to see 3  
        int j = f.y;  // could see 0
    } 
} 

}

5 Answers

I think reading 0 is possible.

The spec says:

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

In our case, we have a write and a read of the same variable, but there is nothing that ensures the read to be subsequent. In particular, the write and read occur in different threads that are not related by any other synchronization action.

That is, it is possible that the read will occur before the write in synchronization order.

This may sound surprising given that the writing thread writes f after y, and the reading thread reads y only if it detects f has been written. But since the write and read to f are not synchronized, the following quote applies:

More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

The explanatory notes to example 17.4.1 also reaffirm that the runtime is permitted to reorder these writes:

If some execution exhibited this behavior, then we would know that instruction 4 came before instruction 1, which came before instruction 2, which came before instruction 3, which came before instruction 4. This is, on the face of it, absurd.

However, compilers are allowed to reorder the instructions in either thread, when this does not affect the execution of that thread in isolation.

In our case, the behavior of the writing thread, in isolation, is not affected by reordering the writes to f and y.

Yes, it is possible to see 0 when

class FinalFieldExample { 
  final int x;
  volatile int y;
  static FinalFieldExample f;
  ...
}

The short explanation:

  • writer() thread publishes f = new FinalFieldExample() object via a data race
  • because of this data race, reader() thread is allowed see f = new FinalFieldExample() object as semi-initialized.
    In particular, reader() thread can see a value of y that was before y = 4; — i.e. initial value 0.

More detailed explanations are here.

You can reproduce this behavior on ARM64 with this jcstress test.

Yes, 0 is possible when x is volatile, because there is no guarantee that the write x = 3 in the writer() thread always happens-before the read local_f.x in the reader() thread.

class FinalFieldExample { 
  volatile int x;
  static FinalFieldExample f;

  public FinalFieldExample() {
    x = 3;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    var local_f = f;
    if (local_f != null) {
        int i = local_f.x;  // could see 0
    }
  }
}

As a result, even though x is volatile (which means that all reads and writes to x happen in a global order), nothing prevents the read local_f.x in the reader() thread from happening before the write x = 3 in the writer() thread.
local_f.x in these case will return 0 (the default value for int, which works like an initial write).

The problem is that after the reader() thread reads f, there is no guarantee (i.e. no happens-before relation) that it sees the inner state on f correctly: i.e. it may not see the write x = 3 into the inner field f.x made by the writer() thread in FinalFieldExample constructor.

You can create this happens-before relation by:

  • either making f volatile (x can be made non-volatile)
    class FinalFieldExample { 
      int x;
      static volatile FinalFieldExample f;
      ...
    }
    
    From the JLS:

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

  • or making x final instead of volatile
    class FinalFieldExample { 
      final int x;
      static FinalFieldExample f;
      ...
    }
    
    From the JLS:

    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.

EDIT: My answer below looks like it's wrong. volatile only requires that all reads and writes (and other "actions") have completed when the write is made, but subsequent writes can still be reordered to occur before the write to the volatile. Thus it is possible to see f before the write to y has occurred.

Which is really weird, but here we are.

user17206833's answer above mine appears to be correct and contains a link to a very useful resource, I suggest you check it out.


Wrong stuff (I'm leaving it up because it illustrate a common misconception):

OP I think I misread your question:

"What if the y field in the following example would be volatile could we observe 0?"

If y is volatile, then no you cannot observe a 0.

class FinalFieldExample { 
  final int x;
  volatile int y; 

If this is what you mean, then the write to y followed by a read of y must create a happens-before edge for the read. The JLS says: "A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field." and never qualifies that statement requiring a read of a reference of some type. The fact that f is neither volatile nor final should make no difference.

Firstly, volatile and initialization are unrelated concepts: A field's initialization guarantees are unaffected by it being volatile or not.

Unless this "escapes" from within the constructor (which is not the case here), the constructor is guaranteed to have completed execution before any other process can access the instance's fields/methods, so y must be initialized in reader() if f != null, ie

int j = f.y;  // will always see 4

See JLS volatile

Related