I try to understand final fields semantic.
Lets research code:
public class App { final int[] data; static App instance; public App() { this.data = new int[]{1, 0}; this.data[1] = 2; } public static void main(String[] args) { new Thread(new Runnable() { public void run() { instance = new App(); } }).start(); while (instance == null) {/*NOP*/} System.out.println(Arrays.toString(instance.data)); } }
I have some questions:
- Does jmm guarantee, that if application terminates then it output [1,2] ?
- Does jmm guarantee that instance.data not null after loop termination?
P.S. I don’t know how to make title correct, feel free to edit.
Additional
Is there visibility difference if we replace:
public App() { this.data = new int[]{1, 0}; this.data[1] = 2; }
with
public App() { int [] data = new int[]{1, 0}; data[1] = 2; this.data = data; }
also I want to know wjat will be if replace final
with volatile in my example.
Thus I want to get explanation about 4 new cases
Advertisement
Answer
Yes, with some catch. You are re-reading the instance
variable after the loop and since both reads are racy, exiting the loop does not guaranty that the read after the loop reads a non-null
reference.
Since this issue is not the topic of the question, assume the following change:
App instance; while((instance=App.instance) == null) {/*NOP*/} System.out.println(Arrays.toString(instance.data));
Then, if application ever terminates, it will output [1,2]
. The point is that the final
field semantics applies to the constructor as a whole, the exact time, when the array reference is written to the field, is irrelevant. This also implies that within the constructor, reorderings are possible, so if the this
reference escapes before the completion of the constructor, all guaranties are void, regardless of whether this
escapes before or after the writes in program order. Since in your code, this
does not escape before the constructor’s completion, the guaranty applies.
Refer to JLS §17.5., final
Field Semantics:
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.
Note that it refers to the completely initialized state, not the write to the particular final
fields. This is also addressed in the next section, §17.5.1:
Let o be an object, and c be a constructor for o in which a
final
field f is written. A freeze action onfinal
field f of o takes place when c exits, either normally or abruptly.
If you change the variable to volatile
, you have almost no guarantees at all. A volatile
field establishes a happens-before relationship between a write to that variable and a subsequent read, but the often overlooked key point is the word “subsequent”. If the App
instance is improperly published, like in your example, there is no guaranty that the main thread’s read of instance.data
will be subsequent. If it reads a null
reference, which is now possible, then you know that it is not subsequent. If it reads a non-null
reference, you know that it is subsequent to the field write, which implies that you are guaranteed to read the 1
in the first slot, but for the second you may read 0
or 2
.
If you want to discuss this in terms of barriers and reordering, the volatile
write to data
guarantees that all previous writes are committed, which includes the write of 1
to the first array slot, but it doesn’t guaranty that subsequent non-volatile
writes are not committed earlier. So it is still possible that the improper publication of the App
reference is performed before the volatile
write (though that rarely happens).
If you move the write to the end of the constructor, all previous writes are visible once a non-null
array reference is seen. For final
fields, it doesn’t need further discussions, as said above, the actual placement of the write within the constructor is irrelevant anyway. For the volatile
case, as said above, you are not guaranteed to read a non-null
reference, but when you read it, all previous writes are committed. It might be helpful to know that the expression new int[]{1, 0};
gets compiled to the equivalent of hiddenVariable=new int[2]; hiddenVariable[0]=1; hiddenVariable[1]=0;
anyway. Placing another array write after its construction but before the volatile
write of the array reference to the field, doesn’t change the semantics.