class Singleton {
// reference to an instance of self class
private static Singleton instance;
// private constructor
private Singleton() {
}
// provide an accessor to obtain instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
}
}
class Singleton {
// reference to an instance of self class, but waste memory
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
// Add lock, but expensive due to `synchronized` overhead
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Reduce overhead of acquiring a lock by testing the lock criterion before acquiring it.
class Singleton {
private static Singleton instance;
private Singleton() {
}
// protect when first created, non-protected when read
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
DCL means only adding lock when it's first time trying to create instance (when is null). If found the instance is already created, no need to enter the critical section which is protected by lock. In this way, Singleton class is allowed concurrently read, but only one thread can write(create) instance at a time (when is the first time the instance is created).
Therefore, compared to 'Hungry pattern, thread safe', which directly add a lock (sychronized) to the whole getInstance()
method, DCL has higher efficiency.
Here are steps of DCL:
- Check that the variable is initialized (without obtaining the lock). If it is initialized, return it immediately.
- Obtain the lock.
- Double-check whether the variable has already been initialized: if another thread acquired the lock first, it may have already done the initialization. If so, return the initialized variable.
- Otherwise, initialize and return the variable.
Volatile (the volatile modifier guarantees that any thread that reads a field will see the most recently written value, avoid rearrangement of instructions). Also, volatile is expensive (link). To make DCL work correctly, declare instance volatile:
private static volatile Singleton instance;
Then, consider the discussion from wiki:
Intuitively, this algorithm(DCL) seems like an efficient solution to the problem. However, this technique has many subtle problems and should usually be avoided. 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. For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object.[6]
- 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.
One of the dangers of using double-checked locking in J2SE 1.4 (and earlier versions) is that it will often appear to work: it is not easy to distinguish between a correct implementation of the technique and one that has subtle problems. Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult.
As of J2SE 5.0, this problem has been fixed. The volatile keyword now ensures that multiple threads handle the singleton instance correctly.
class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
Singleton localInstance = instance;
if (localInstance == null) {
synchronized(Singleton.class) {
if (localInstance == null) {
instance = localInstance = new Singleton();
}
}
}
return localInstance;
}
}
Note the local variable "localInstance", which seems unnecessary. The effect of this is that in cases where instance is already initialized (i.e., most of the time), the volatile field is only accessed once (due to
return localInstance;
instead ofreturn instance;
), which can improve the method's overall performance by as much as 40 percent.[7]