Immutability and reordering

后端 未结 10 643
星月不相逢
星月不相逢 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:54

    Nothing sets the reference to null once it is non-null. It's possible for a thread to see null after another thread has set it to non-null but I don't see how the reverse is possible.

    I'm not sure instruction re-ordering is a factor here, but interleaving of instructions by two threads is. The if branch can't somehow be reordered to execute before its condition has been evaluated.

    0 讨论(0)
  • 2020-12-04 10:57

    UPDATE Feb10

    I'm getting convinced that we should separate 2 phases: compilation and execution.

    I think that the decision factor whether it is allowed to return null or not is what the bytecode is. I made 3 examples:

    Example 1:

    The original source code, literally translated to bytecode:

    if (resource == null)
        resource = new Resource();  // unsafe publication
    return resource;
    

    The bytecode:

    public static Resource getInstance();
    Code:
    0:   getstatic       #20; //Field resource:LResource;
    3:   ifnonnull       16
    6:   new             #22; //class Resource
    9:   dup
    10:  invokespecial   #24; //Method Resource."<init>":()V
    13:  putstatic       #20; //Field resource:LResource;
    16:  getstatic       #20; //Field resource:LResource;
    19:  areturn
    

    This is the most interesting case, because there are 2 reads (Line#0 and Line#16), and there is 1 write inbetween (Line#13). I claim that it is not possible to reorder, but let's examine it below.

    Example 2:

    The "complier optimized" code, which can be literally re-converted to java as follows:

    Resource read = resource;
    if (resource==null)
        read = resource = new Resource();
    return read;
    

    The byte code for that (actually I produced this by compiling the above code snippet):

    public static Resource getInstance();
    Code:
    0:   getstatic       #20; //Field resource:LResource;
    3:   astore_0
    4:   getstatic       #20; //Field resource:LResource;
    7:   ifnonnull       22
    10:  new     #22; //class Resource
    13:  dup
    14:  invokespecial   #24; //Method Resource."<init>":()V
    17:  dup
    18:  putstatic       #20; //Field resource:LResource;
    21:  astore_0
    22:  aload_0
    23:  areturn
    

    It is obvious, that if the compiler "optimizes", and the byte code like above is produced, a null read can occur (for example, I refer to Jeremy Manson's blog)

    It is also interesting to see that how a = b = c is working: the reference to new instance (Line#14) is duplicated (Line#17), and the same reference is stored then, first to b (resource, (Line#18)) then to a (read, (Line#21)).

    Example 3:

    Let's make an even slighter modification: read the resource only once! If the compiler starts to optimize (and using registers, as others mentioned), this is better optimization than above, because Line#4 here is a "register access" rather than a more expensive "static access" in Example 2.

    Resource read = resource;
    if (read == null)   // reading the local variable, not the static field
        read = resource = new Resource();
    return read;
    

    The bytecode for Example 3 (also created with literally compiling the above):

    public static Resource getInstance();
    Code:
    0:   getstatic       #20; //Field resource:LResource;
    3:   astore_0
    4:   aload_0
    5:   ifnonnull       20
    8:   new     #22; //class Resource
    11:  dup
    12:  invokespecial   #24; //Method Resource."<init>":()V
    15:  dup
    16:  putstatic       #20; //Field resource:LResource;
    19:  astore_0
    20:  aload_0
    21:  areturn
    

    It is also easy to see, that it is not possible to get null from this bytecode since it is constructed the same way as String.hashcode(), having only 1 read of the static variable of resource.

    Now let's examine Example 1:

    0:   getstatic       #20; //Field resource:LResource;
    3:   ifnonnull       16
    6:   new             #22; //class Resource
    9:   dup
    10:  invokespecial   #24; //Method Resource."<init>":()V
    13:  putstatic       #20; //Field resource:LResource;
    16:  getstatic       #20; //Field resource:LResource;
    19:  areturn
    

    You can see that Line#16 (the read of variable#20 for return) most observe the write from Line#13 (the assignation of variable#20 from the constructor), so it is illegal to place it ahead in any execution order where Line#13 is executed. So, no reordering is possible.

    For a JVM it is possible to construct (and take advantage of) a branch that (using certain extra conditions) bypasses the Line#13 write: the condition is that the read from variable#20 must not be null.

    So, in neither case for Example 1 is possible to return null.

    Conclusion:

    Seeing the examples above, a bytecode seen in Example 1 WILL NOT PRODUCE null. An optimized bytecode like in Example 2 WILL PROCUDE null, but there is an even better optimization Example 3, which WILL NOT PRODUCE null.

    Because we cannot be prepared for all possible optimization of all the compilers, we can say that in some cases it is possible, some other cases not possible to return null, and it all depends on the byte code. Also, we have shown that there is at least one example for both cases.


    Older reasoning: Referring for the example of Assylias: The main question is: is it valid (concerning all specs, JMM, JLS) that a VM would reorder the 11 and 14 reads so, that 14 will happen BEFORE 11?

    If it could happen, then the independent Thread2could write the resource with 23, so 14 could read null. I state that it is not possible.

    Actually, because there is a possible write of 13, it would not be a valid execution order. A VM may optimize the execution order so, that excludes the not-executed branches (remaining just 2 reads, no writes), but to make this decision, it must do the first read (11), and it must read not-null, so the 14 read cannot precede the 11 read. So, it is NOT possible to return null.


    Immutability

    Concerning immutability, I think that this statement is not true:

    UnsafeLazyInitialization is actually safe if Resource is immutable.

    However, if the constructor is unpredictable, interesting results may come out. Imagine a constructor like this:

    public class Resource {
        public final double foo;
    
        public Resource() {
            this.foo = Math.random();
        }
    }
    

    If we have tho Threads, it may result, that the 2 threads will receive a differently-behaving Object. So, the full statement should sound like this:

    UnsafeLazyInitialization is actually safe if Resource is immutable and its initialization is consistent.

    By consistent I mean that calling the constructor of the Resource twice we will receive two objects that behave exactly the same way (calling the same methods in the same order on both will yield the same results).

    0 讨论(0)
  • 2020-12-04 10:59

    After reading through the post you linked more carefully, you are correct, the example you posted could conceivably (under the current memory model) return null. The relevant example is way down in the comments of the post, but effectively, the runtime can do this:

    public class UnsafeLazyInitialization {
        private static Resource resource;
    
        public static Resource getInstance() {
            Resource tmp = resource;
            if (resource == null)
                tmp = resource = new Resource();  // unsafe publication
            return tmp;
        }
    }
    

    This obeys the constraints for a single-thread, but could result in a null return value if multiple threads are calling the method (the first assignment to tmp gets a null value, the if block sees a non-null value, tmp gets returned as null).

    In order to make this "safely" unsafe (assuming Resource is immutable), you have to explicitly read resource only once (similar to how you should treat a shared volatile variable:

    public class UnsafeLazyInitialization {
        private static Resource resource;
    
        public static Resource getInstance() {
            Resource cur = resource;
            if (cur == null) {
                cur = new Resource();
                resource = cur;
            }
            return cur;
        }
    }
    
    0 讨论(0)
  • 2020-12-04 10:59

    This is now a very long back thread, still given this question discusses many interesting workings of re-ordering and concurrency, I am involving here by though lately.

    For a moment, if we do not involve concurrency, the actions and valid reorderings in multi-threaded situation.
    "Can JVM use a cached value post write operation in single-thread context". I think no. Given there is a write operation in if condition can caching come in to play at all.
    So back to the question, immutability ensure that the object is fully or correctly created before it's reference is accessible or published, so immutability definitely helps. But here there is a write operation after the object creation. So can the second read cache the value from pre-write, in the same thread or another. No. One thread might not know about the write in other thread (given there is no need for immediate visibility between threads). So won't the possibility of returning a false null (i.e after the object creation) be invalid. ( The code in question breaks singleton, but we are not bothered about the here)

    0 讨论(0)
提交回复
热议问题