Let's consider the classic double checked locking example to understand why a reference needs to be atomic :
class Foo {
private Helper result;
public static Helper getHelper() {
if (result == null) {//1
synchronized(Foo.class) {//2
if (result == null) {//3
result = new Helper();//4
}
}
}
return result//5;
}
// other functions and members...
}
Let's consider 2 threads that are going to call the getHelper method :
- Thread-1 executes line number 1 and finds
result to be null.
- Thread-1 acquires a class level lock on line number 2
- Thread-1 finds
result to be null on line number 3
- Thread-1 starts instantiating a new
Helper
- While Thread-1 is still instantiating a new
Helper on line number 4, Thread-2 executes line number 1.
Steps 4 and 5 is where an inconsistency can arise. There is a possibility that at Step 4, the object is not completely instantiated but the result variable already has the address of the partially created Helper object stamped into it. If Step-5 executes even a nanosecond before the Helper object is fully initialized,Thread-2 will see that result reference is not null and may return a reference to a partially created object.
A way to fix the issue is to mark result as volatile or use a AtomicReference. That being said, the above scenario is highly unlikely to occur in the real world and there are better ways to implement a Singleton than using double-checked locking.
Here's an example of implementing double-checked locking using AtomicReference :
private static AtomicReference instance = new AtomicReference();
public static AtomicReferenceSingleton getDefault() {
AtomicReferenceSingleton ars = instance.get();
if (ars == null) {
instance.compareAndSet(null,new AtomicReferenceSingleton());
ars = instance.get();
}
return ars;
}
If you are interested in knowing why Step 5 can result in memory inconsistencies, take a look at this answer (as suggested by pwes in the comments)