A good mental approach here would be to separate threads into two categories: those that can instantiate the class, and those that can't. (For conciseness, I'll shorten the class name to just Singleton
). Then you have to think about what each category of threads needs to do:
- instantiating threads need to store the reference they create in
instance
and return it
- all other threads need to wait until
instance
has been set, and then return it
Additionally, we need to ensure two things:
- That there is a happens-before edge between the instantiation and all returns (including ones in non-instantiating threads). This is for thread safety.
- That the set of instantiating threads has exactly one element (assuming either set is non-empty, of course). This is to ensure that there's only one instance.
Okay, so those are our four requirements. Now we can write code that satisfies them.
private final AtomicBoolean instantiated = new AtomicBoolean(false);
private static volatile Singleton instance = null;
// volatile ensures the happens-before edge
public static Singleton getInstance() {
// first things first, let's find out which category this thread is in
if (instantiated.compareAndSet(false, true) {
// This is the instantiating thread; the CAS ensures only one thread
// gets here. Create an instance, store it, and return it.
Singleton localInstance = new Singleton();
instance = localInstance;
return localInstance;
} else {
// Non-instantiating thread; wait for there to be an instance, and
// then return it.
Singleton localInstance = instance;
while (localInstance == null) {
localInstance = instance;
}
return localInstance;
}
}
Now, let's convince ourselves that each one of our conditions are met:
- Instantiating thread creates an instance, stores it, and returns it: this is the "true" block of the CAS.
- Other threads wait for an instance to be set, and then return it: That's what the
while
loop does.
- There is a happens-before (HB) edge between instantiation and returning: For the instantiating thread, within-thread semantics ensure this. For all other threads, the
volatile
keyword ensures a HB edge between the write (in the instantiating thread) and the read (in this thread)
- That the set of instantiating threads is exactly one large, assuming the method is ever invoked: The first thread to hit the CAS will have it return true; all others will return false.
So we're all set.
The general advice here is to break down your requirements into sub-requirements that are as specific as possible. Then you can address each one separately, which is easier to reason about.