Audrey
Audrey

Reputation: 2238

Why is this double-checked lock implemented with a separate wrapper class?

When I was reading Wikipedias's article about Double Checked Locking idiom, I'm confused about its implementation:

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) { 
        this.value = value; 
    }
} 
public class Foo {
    private FinalWrapper<Helper> helperWrapper = null;

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

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

I simply don't understand why we need to create wrapper. Isn't this enough ?

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

Is it because using wrapper can speed up initialization because wrapper is stored on stack and helperWrapper is stored in heap?

Upvotes: 11

Views: 468

Answers (3)

Patrick Parker
Patrick Parker

Reputation: 4959

Simply using helperWrapper for both null checks and the return statement could fail due to read reordering allowed under the Java Memory Model.

Here is the example scenario:

  1. The first helperWrapper == null (racy read) test evaluates to false, i.e. helperWrapper is not null.
  2. The final line,return helperWrapper.value (racy read) results in a NullPointerException, i.e. helperWrapper is null

How did that happen? The Java Memory Model allows those two racy reads to be reordered, because there was no barrier prior to the read i.e. no "happens-before" relationship. (See String.hashCode example)

Notice that before you can read helperWrapper.value, you must implicitly read the helperWrapper reference itself. So the guarantees provided by final semantics that helperWrapper is fully instantiated do not apply, because they only apply when helperWrapper is not null.

Upvotes: 3

gnat
gnat

Reputation: 6222

Isn't this enough ?

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

No this isn't enough.

Above, first check for helperWrapper == null is not thread safe. It may return false (seeing non-null instance) for some thread "too early", pointing to not fully constructed helperWrapper object.

The very Wikipedia article you refer to, explains this issue step-by-step:

For example, consider the following sequence of events:

  1. Thread A notices that the value is not initialized, so it obtains the lock and begins to initialize the value.
  2. Due to the semantics of some programming languages, the code generated by the compiler is allowed to update the shared variable to point to a partially constructed object before A has finished performing the initialization.
  3. Thread B notices that the shared variable has been initialized (or so it appears), and returns its value. Because thread B believes the value is already initialized, it does not acquire the lock. If B uses the object before all of the initialization done by A is seen by B (either because A has not finished initializing it or because some of the initialized values in the object have not yet percolated to the memory B uses (cache coherence)), the program will likely crash.

Note semantics of some programming languages mentioned above is exactly the semantic of Java as of version 1.5 and higher. Java Memory Model (JSR-133) explicitly allows for such behavior - search the web for more details on that if you're interested.

Is it because using wrapper can speed up initialization because wrapper is stored on stack and helperWrapper is stored in heap?

No, above isn't the reason.

The reason is thread safety. Again, semantic of Java 1.5 and higher (as defined in Java Memory Model) guarantees that any thread will be able to access only properly initialized Helper instance from wrapper due to the fact that it is a final field initialized in constructor - see JLS 17.5 Final Field Semantics.

Upvotes: 1

Cyberfox
Cyberfox

Reputation: 1145

My understanding is that the reason wrapper is used is that reading an immutable object (all fields are final) is an atomic operation.

If wrapper is null, then it's not yet an immutable object, and we have to fall into the synchronized block.

If wrapper is not-null then we are guaranteed to have a fully constructed value object, so we can return it.

In your code, the null check could be done without actually reading the referenced object, and so not triggering the atomicity of the operation. I.e. the operation could have initialized helperWrapper in preparation for passing the result of new Helper() to the constructor, but the constructor has not been called yet.

Now I would presume that the subsequent code in your example would read return helperWrapper.value; which should trigger an atomic reference read, guaranteeing that the constructor completes, but it's entirely possible ('semantics of some programming languages') that the compiler is allowed to optimize that to not perform an atomic read, and thus it would return an incompletely initialized value object under the exact right circumstances.

Performing the barrier using a local variable and a reference copy forces the read and write to be atomic, and ensures that the code is thread-safe.

I believe the key understanding is that immutable reference reads and writes are atomic, so assignment to another variable is atomic, but null testing might not be.

Upvotes: -1

Related Questions