问题
Below code sample is taken from JLS 17.5 "final Field Semantics":
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
}
}
}
Since the instance of FinalFieldExample
is published through a data race, is it possible that the f != null
check evaluates successfully, yet subsequent f.x
dereference sees f
as null
?
In other words, is it possible to get a NullPointerException
on line that is commented with "guaranteed to see 3"?
回答1:
Okay, here is my own take on it, based on quite a detailed talk (in Russian) on final semantics given by Vladimir Sitnikov, and subsequent revisit of JLS 17.5.1.
Final field semantics
The specification states:
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).
In other words, we are guaranteed to see the write to a final field if the following chain of relations can be built:
hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)
1. hb(w, f)
w is the write to the final field: x = 3
f is the "freeze" action (exiting FinalFieldExample
constructor):
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, either normally or abruptly.
As the field write comes before finishing the constructor in program order, we can assume that hb(w, f)
:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)
2. hb(f, a)
Definition of a given in the specification is really vague ("action, that is not a read of a final field")
We can assume that a is publishing a reference to the object (f = new FinalFieldExample()
) since this assumption does not contradict the spec (it is an action, and it is not a read of a final field)
Since finishing constructor comes before writing the reference in program order, these two operations are ordered by a happens-before relationship: hb(f, a)
3. mc(a, r1)
In our case r1 is a "read of the final field frozen by f" (f.x
)
And this is where it starts to get interesting. mc (Memory Chain) is one of the two additional partial orders introduced in "Semantics of final Fields" section:
There are several constraints on the memory chain ordering:
- If r is a read that sees a write w, then it must be the case that mc(w, r).
- If r and a are actions such that dereferences(r, a), then it must be the case that mc(r, a).
- If w is a write of the address of an object o by a thread t that did not initialize o, then there must exist some read r by thread t that sees the address of o such that mc(r, w).
For the simple example given in question we're really only interested in the first point, as the other two are needed to reason about more complicated cases.
Below is the part that actually explains why it is possible to get an NPE:
- notice the bold part in the spec quote:
mc(a, r1)
relation only exists if the read of the field sees the write to the shared reference f != null
andf.x
are two distinct read operations from the JMM standpoint- there is nothing in the spec that says that
mc
relations are transitive with respect to program-order or happens-before - therefore if
f != null
sees the write done by another thread, there are no guarantees thatf.x
sees it too
I won't go into the details of the Dereference Chain constraints, as they are needed only to reason about longer reference chains (e.g. when a final field refers to an object, which in turn refers to another object).
For our simple example it suffices to say that JLS states that "dereferences order is reflexive, and r1 can be the same as r2" (which is exactly our case).
Safe way of dealing with unsafe publication
Below is the modified version of the code that is guaranteed to not throw an NPE:
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() {
FinalFieldExample local = f;
if (local != null) {
int i = local.x; // guaranteed to see 3
int j = local.y; // could see 0
}
}
}
The important difference here is reading the shared reference into a local variable. As stated by JLS:
Local variables ... are never shared between threads and are unaffected by the memory model.
Therefore, there is only one read from shared state from the JMM standpoint.
If that read happens to see the write done by another thread, it would imply the two operations are connected with a memory chain (mc
) relationship.
Furthermore, local = f
and i = local.x
are connected with dereference chain relationship, which gives us the whole chain mentioned in the beginning:
hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)
回答2:
Your analysis is beautiful (1+), if I could upvote twice - I would. Here is one more link to the same problem with "independent reads" here, for example.
I have also tried to approach this problem in a different answer too.
I think if we introduce the same concept here, things could be provable, too. Let's take that method and slightly change it:
static void reader() {
FinalFieldExample instance1 = f;
if (instance1 != null) {
FinalFieldExample instance2 = f;
int i = instance2.x;
FinalFieldExample instance3 = f;
int j = instance3.y;
}
}
And a compiler can now do some eager reads (move those reads before the if statement
):
static void reader() {
FinalFieldExample instance1 = f;
FinalFieldExample instance2 = f;
FinalFieldExample instance3 = f;
if (instance1 != null) {
int i = instance2.x;
int j = instance3.y;
}
}
Those reads can be further re-ordered between them:
static void reader() {
FinalFieldExample instance2 = f;
FinalFieldExample instance1 = f;
FinalFieldExample instance3 = f;
if (instance1 != null) {
int i = instance2.x;
int j = instance3.y;
}
}
Things should be trivial from here: ThreadA
reads FinalFieldExample instance2 = f;
to be null
, before it does the next read : FinalFieldExample instance1 = f;
some ThreadB
calls writer
(as such f != null
) and the part:
FinalFieldExample instance1 = f;
is resolved to non-null
.
来源:https://stackoverflow.com/questions/62278680/can-object-access-be-reordered-with-that-objects-final-field-access-in-java