javafinaljava-memory-modelinstruction-reordering

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


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"?


Solution

  • 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:

    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)