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)