Reputation: 2238
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
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:
helperWrapper == null
(racy read) test evaluates to false, i.e. helperWrapper is not null.return helperWrapper.value
(racy read) results in a NullPointerException, i.e. helperWrapper is nullHow 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
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:
- Thread A notices that the value is not initialized, so it obtains the lock and begins to initialize the value.
- 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.
- 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
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