Java lazy thread safe singleton with implemented w

2019-08-22 12:03发布

问题:

I do not understand why local variable is needed here:

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper;

   public Helper getHelper() {
      FinalWrapper<Helper> tempWrapper = helperWrapper;

      if (tempWrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              tempWrapper = helperWrapper;
          }
      }
      return tempWrapper.value;
   }
}

I get this code from: https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java. What issues can we have if we do not have this local variable? According to the wiki article:

Semantics of final field in Java 5 can be employed to safely publish the helper object without using volatile. The local variable tempWrapper is required for correctness: simply using helperWrapper for both null checks and the return statement could fail due to read reordering allowed under the Java Memory Model. Performance of this implementation is not necessarily better than the volatile implementation.

Thanks in advance.

回答1:

To understand the underlying issue let's remove the local variable from the code:

public class Foo {
    private FinalWrapper<Helper> helperWrapper;

    public Helper getHelper() {
        if (helperWrapper == null) {
            synchronized(this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<Helper>(new Helper());
                }
            }
        }
        return helperWrapper.value;
    }
}

We have three reads in this case:

  1. The outer null check.
  2. The inner null check.
  3. The read before the return.

The problem is that due to the read reordering the first read can return a non-null value and the third read can return null. It means that the third read happens before the first one, which is supposed to ensure helperWrapper is initialized...

Adding the local variable solves the issue because we assign helperWrapper value to tempWrapper and then it does not matter in what order tempWrapper is read. If it has a non-null value it is used both for the null check and for the return statement.

It can happen because Java Memory Model allows for such reordering of operations just for the optimization purpose. Look at the quote from here:

What is meant by reordering?

There are a number of cases in which accesses to program variables (object instance fields, class static fields, and array elements) may appear to execute in a different order than was specified by the program. The compiler is free to take liberties with the ordering of instructions in the name of optimization. Processors may execute instructions out of order under certain circumstances. Data may be moved between registers, processor caches, and main memory in different order than specified by the program.

[...]

The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings. However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.

[...]