Immutability and reordering

后端 未结 10 665
星月不相逢
星月不相逢 2020-12-04 10:07

The code below (Java Concurrency in Practice listing 16.3) is not thread safe for obvious reasons:

public class UnsafeLazyInitialization {
    private static         


        
10条回答
  •  旧巷少年郎
    2020-12-04 10:41

    After applying the JLS rules to this example, I have come to the conclusion that getInstance can definitely return null. In particular, JLS 17.4:

    The memory model determines what values can be read at every point in the program. The actions of each thread in isolation must behave as governed by the semantics of that thread, with the exception that the values seen by each read are determined by the memory model.

    It is then clear that in the absence of synchronization, null is a legal outcome of the method since each of the two reads can observe anything.


    Proof

    Decomposition of reads and writes

    The program can be decomposed as follows (to clearly see the reads and writes):

                                  Some Thread
    ---------------------------------------------------------------------
     10: resource = null; //default value                                  //write
    =====================================================================
               Thread 1               |          Thread 2                
    ----------------------------------+----------------------------------
     11: a = resource;                | 21: x = resource;                  //read
     12: if (a == null)               | 22: if (x == null)               
     13:   resource = new Resource(); | 23:   resource = new Resource();   //write
     14: b = resource;                | 24: y = resource;                  //read
     15: return b;                    | 25: return y;                    
    

    What the JLS says

    JLS 17.4.5 gives the rules for a read to be allowed to observe a write:

    We say that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

    • r is not ordered before w (i.e., it is not the case that hb(r, w)), and
    • there is no intervening write w' to v (i.e. no write w' to v such that hb(w, w') and hb(w', r)).

    Application of the rule

    In our example, let's assume that thread 1 sees null and properly initialises resource. In thread 2, an invalid execution would be for 21 to observe 23 (due to program order) - but any of the other writes (10 and 13) can be observed by either read:

    • 10 happens-before all actions so no read is ordered before 10
    • 21 and 24 have no hb relationship with 13
    • 13 does not happens-before 23 (no hb relationship between the two)

    So both 21 and 24 (our 2 reads) are allowed to observe either 10 (null) or 13 (not null).

    Execution path that returns null

    In particular, assuming that Thread 1 sees a null on line 11 and initialises resource on line 13, Thread 2 could legally execute as follows:

    • 24: y = null (reads write 10)
    • 21: x = non null (reads write 13)
    • 22: false
    • 25: return y

    Note: to clarify, this does not mean that T2 sees non null and subsequently sees null (which would breach the causality requirements) - it means that from an execution perspective, the two reads have been reordered and the second one was committed before the first one - however it does look as if the later write had been seen before the earlier one based on the initial program order.

    UPDATE 10 Feb

    Back to the code, a valid reordering would be:

    Resource tmp = resource; // null here
    if (resource != null) { // resource not null here
        resource = tmp = new Resource();
    }
    return tmp; // returns null
    

    And because that code is sequentially consistent (if executed by a single thread, it will always have the same behaviour as the original code) it shows that the causality requirements are satisfied (there is a valid execution that produces the outcome).


    After posting on the concurrency interest list, I got a few messages regarding the legality of that reordering, which confirm that null is a legal outcome:

    • The transformation is definitely legal since a single-threaded execution won't tell the difference. [Note that] the transformation doesn't seem sensible - there's no good reason a compiler would do it. However, given a larger amount of surrounding code or perhaps a compiler optimization "bug", it could happen.
    • The statement about intra-thread ordering and program order is what made me question the validity of things, but ultimately the JMM relates to the bytecode that gets executed. The transformation could be done by the javac compiler in which case null will be perfectly valid. And there are no rules for how javac has to convert from Java source to Java bytecode so...

提交回复
热议问题