4

I was reading this: https://en.m.wikipedia.org/wiki/Double-checked_locking

And in section Usage in Java, last example:

Semantics of final field in Java 5 can be employed to safely publish the helper object without using volatile:

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;
   }
}

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.[14] Performance of this implementation is not necessarily better than the volatile implementation.

Why tempWrapper is required for correctness? Can't I just remove it and replace with helperWrapper. As far as I understand, the reference to object being created of FinalWrapper<Helper> won't escape to other threads before it's final field value has been initialized in constructor. If some other thread read helperWrapper as not null, then it must have correct value for value.

5
  • 1
    J. Bloch reflects on this in Effective Java (section: 11 Concurrency | Item 83: Use lazy initialization judiciously). However, he only demonstrates the double-check idiom with a volatile instance field. Concurrency in Practice (by Goetz) also has a section on this: Double-checked locking. There, DCL is considered to be an antipattern; and it is explicitly stated that it only works correctly if the field is volatile. Commented Nov 2, 2024 at 7:33
  • 1
    @JanezKuhar The pattern with the final wrapper not being mentioned doesn't mean it isn't safe (and I think it should work given that final makes sure the object is fully initialized before it's set etc). Commented Nov 2, 2024 at 7:49
  • I've also found this answer by Eugene that demonstrates how a compiler might reorder operations. Commented Nov 2, 2024 at 7:52
  • @dan1st True. Also found a blog post by Aleksey Shipilëv (OpenJDK contributor) that validates this pattern. Commented Nov 2, 2024 at 8:32
  • This blog post also mentions "The introduction of local variable here is a correctness fix, but only partial: [...]" for what the author asked. Commented Nov 2, 2024 at 9:38

2 Answers 2

1

The getHelper method, if refactored by replacing tempWrapper with helperWrapper, will look like the following:

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

Referring SSA optimization and also this SO answer, the compiler may optimize the above version of getHelper method, with introduction of local vars and reordering the assignments, to look like the following:

public Helper getHelper() {
  // <--- Reordered by compiler here, in the of the scenarios
  FinalWrapper compilerVar4 = helperWrapper;
  FinalWrapper compilerVar1 = helperWrapper;

  if (compilerVar1 == null) {
    synchronized(this) {
      FinalWrapper compilerVar2 = helperWrapper;
      if (compilerVar2 == null) {
        FinalWrapper compilerVar3 = new FinalWrapper<Helper>(new Helper());
        helperWrapper = compilerVar3;
        // <--- Added by compiler
        return compilerVar3.value;
      }
    }
  }

  // <--- Reordered by the compiler (moved to the top)
  // FinalWrapper compilerVar4 = helperWrapper;
  return compilerVar4.value;
}

Now let's say the compilerVar4 reads a value of null, whereas in the next line the compilerVar1 could read a non-null value in the case that another thread has changed the value of helperWrapper by the time this 2nd read occurs. And thus the method will return a null incorrectly (as compilerVar1 == null will return false now).

This issue of returning a null (when the value is in fact a non-null) is avoided by the introduction of the local variable tempWrapper.


As an afterthought, in my understanding the initialization-on-demand holder pattern enables a safe, highly concurrent lazy initialization of static fields with good performance, as recommended in the above Wiki Double-checked locking (Usage in Java) and is referred to in this SO answer.

class Foo {
    private static class HelperHolder {
       public static final Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

Why tempWrapper is required for correctness? Can't I just remove it and replace with helperWrapper.

Let's compare the following 2 versions:

Original version:

   public Helper getHelper() {
      var localVar = helperWrapper; // read #1 of helperWrapper

      if (localVar == null) {
          synchronized (this) {
              ...
          }
      }
      return localVar.value;
   }

Version where tempWrapper is replaced with helperWrapper:

   public Helper getHelper() {
      if (helperWrapper == null) {  // read #1 of helperWrapper
          synchronized (this) {
              ...
          }
      }
      return helperWrapper.value;  // read #2 of helperWrapper
   }

Notice the number of reads of the shared variable helperWrapper:

  • 1 read in the original version
  • 2 reads in the modified version

If the write to helperWrapper happened in another thread, then the JMM treats such reads as kind of independent, i.e. it permits executions where the 1st read returns a non-null value and the 2nd read returns null (in this example this will throw NPE).
Therefore the local variable is used to avoid the 2nd read of the shared variable helperWrapper.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.