Assuming that we have two threads started, like this:
new Thread(FinalFieldExample::writer).start(); // Thread #1
new Thread(FinalFieldExample::reader).start(); // Thread #2
We might observe our program's actual order of operations to be the following:
Thread #1
writes x = 3
.
Thread #1
writes f = ...
.
Thread #2
reads f
and finds that it is not null
.
Thread #2
reads f.x
and sees 3
.
Thread #2
reads f.y
and sees 0
, because y
does not appear to be written yet.
Thread #1
writes y = 4
.
In other words, Threads #1
and #2
are able to have their operations interleave in a way such that Thread #2
reads f.y
before Thread #1
writes it.
Note also that the write to the static
field f
was allowed to be reordered so that it appears to happen before the write to f.y
. This is just another consequence of the absence of any kind of synchronization. If we declared f
as also volatile
, this reordering would be prevented.
There's some talk in the comments about writing to final
fields with reflection, which is true. This is discussed in §17.5.3:
In some cases, such as deserialization, the system will need to change the final
fields of an object after construction. final
fields can be changed via reflection and other implementation-dependent means.
It's therefore possible in the general case for Thread #2
to see any value when it reads f.x
.
There's also a more conventional way to see the default value of a final
field, by simply leaking this
before the assignment:
class Example {
final int x;
Example() {
leak(this);
x = 5;
}
static void leak(Example e) { System.out.println(e.x); }
public static void main(String[] args) { new Example(); }
}
I think that if FinalFieldExample
's constructor was like this:
static FinalFieldExample f;
public FinalFieldExample() {
f = this;
x = 3;
y = 4;
}
Thread #2
would be able to read f.x
as 0
as well.
This is from §17.5:
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.
The more technical sections of specification for final
contain wording like that as well.