Can object access be reordered with that object's final field access in Java?

耗尽温柔 提交于 2020-06-25 04:41:08

问题


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 and f.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 that f.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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!