Is it guaranteed that volatile field would be properly initialized
Asked Answered
E

5

6

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
    } 
} 

}

Ewing answered 19/10, 2021 at 22:25 Comment(11)
The semantics are similar but not the same. volatile guarantees that all actions (reads and writes) that happen before the write to the volatile are visible. final guarantees that only that particular field (and any object the field refers to) is visible. final is called out explicitly to NOT have a transitive relationship with all other actions on a thread. (So, previous reads and writes on a thread are unaffected by a final field.)Arsonist
@mark no: final means the value (which can be a reference) is assigned exactly once, and assignment happens during object initialization - it may never be reassigned. final has nothing to do with visibility.Delila
You are 100% wrong about that. Read the link the OP provided. I'd also recommend Brian Geotz's book, Java Concurrency in Practice. This is well known semantics for Java's final keyword.Arsonist
final is for initialization and volatile is about updating the variable at the same.Finical
@Arsonist can you provide a JCIP citation?Delila
"...Let o be an object, and c be a constructor for o in which a final field f is written. A freeze action on final field f of o takes place when c exits...Given a write w, a freeze f, an action a (that is not a read of a final field), a read r1 of the final field frozen by f, and a read r2 such that hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2), then when determining which values can be seen by r2, we consider hb(w, r2). (This happens-before ordering does not transitively close with other happens-before orderings.)"Arsonist
Note that this freeze-action is distinct from a synchronizes-with but still creates a happens-before relationship. (And Brian Goetz's book is a much easier read that trying to puzzle out the spec.)Arsonist
@mark I don't see the word "visibility" anywhere in that text. final doesn't need volatile because its value never changes, but that has nothing to do with visibility. Are you still sure "I'm 100% wrong"?Delila
Er, I don't think the OP is asking if the field needs to be both volatile and final. It's one or the other. And happens-before means "visible" in the JLS. You need to read that entire chapter for it to make sense: JLS section 17.4.5. Happens-before Order "Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second." I'm not really interested in spoon feeding this to you. Please make an effort on your own, or at least ask a question here that someone can answer. @DelilaArsonist
OP: look at user17206833's answer below. It appears to (very cogently) explain that y can be 0 even if volatile. Something I was not aware of, and seems to be a subtle point in the Java memory model.Arsonist
as to the other question that you posted, I will provide the same comment. This is a "data race", yes, zero is such a possibility. The guarantees for final in the spec, are for all fields, your example violates that. But it is very tricky, I agree. Although I really like these things, I sometimes get them wrong too.Satterwhite
I
5

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.

Incarnation answered 21/10, 2021 at 2:6 Comment(1)
you need to look closely at the jcstress example + OP's example. There is a final in OPs example, which, in the current implementations (I've looked at 8 to 17), makes a big difference: zero is not possible. But JLS allows it.Satterwhite
P
4

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.

Prehension answered 20/10, 2021 at 1:5 Comment(1)
it is possible according to the JLS, since this is a data race, yes. It is impossible in the current implementation of the JVM (strictly taking into account OPs example), because it takes at least one final field for the proper barriers to be inserted.Satterwhite
M
1

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.

Mezzotint answered 21/10, 2021 at 7:0 Comment(0)
A
0

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.

Arsonist answered 20/10, 2021 at 2:58 Comment(5)
How do you know the read "follows" the write? Are you assuming the Java Memory Model to be sequentially consistent? It's not.Prehension
Correct. That why I am asking how you know this condition to be true?Prehension
I'm not sure I follow. The OP is asking if a read of y will result in a 4 or a 0, after y is written with a 4 in the ctor when y is declared volatile (I think we and the OP all agree that if y is not volatile then of course it could be 0). Surely this read of y will be 4 after the write of 4 in the ctor. (The read only happens if f has been assigned (the OP checks for null) and f can only be assigned if the ctor has run. I don't see any other possible code path.)Arsonist
So you're assuming that operations done in different threads execute one after the other, in some particular order. That is known as sequential consistency. The problem is that the Java Memory Model guarantees sequential consistency only for correctly synchronized code, but the OP's code is not correctly synchronized, since f is shared through a data race. Incorrectly synchronized code need not be sequentially consistent, i.e. different threads may have a different perspective and perceive memory updates in a different order from each other.Prehension
user17206833's answer above is starting to convince me I'm wrong. I find it a little shocking that a write (of any field) could be reordered before a write to a volatile but it appears the JLS does allow this. I'm going to poke at this a bit more and see what I get. (Also user17208506's answer below mine appears to be correct and deserves an up-vote.)Arsonist
D
-2

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

Delila answered 19/10, 2021 at 23:51 Comment(7)
Does it mean I could observe f.y as 0? (when y is volatile)Ewing
@Ewing It is very unlikely and I don't think so. Because in your example, if you say that you could see 0, that means even both methods writer() and reader() are synchronized, it does not guarantee that the field has been initialized; isn't that break the whole language itself.Finical
@Ewing No. For f to be not null, the initialization of the instance must have completed: The assignment of the instance to the variable f happens after instance initialization. See edits to answer.Delila
@Finical It is not possible to see 0 at the line in question.Delila
@Delila Yes, that's what I thought. It's all because of one line: if(f != null).Finical
In reasoning "For f to be not null, the initialization of the instance must have completed" you are assuming sequential consistency, but the Java Memory Model guarantees sequential consistency only for correctly synchronized code, which this code is not, since f is shared through a data race.Prehension
you need to look at the JLS guarantees, and in OPs example, there is a data race, zero is possible. This answer is wrong.Satterwhite

© 2022 - 2024 — McMap. All rights reserved.