DCL stands for Double Check Lock. In fact, many people have used DCL in singleton mode, and LZ also asked them to write it when interviewing people, but many people always make mistakes. Why did they get it wrong? What is the source of its error? What are the solutions? Let’s analyze it with LZ
Problem analysis
Let’s first look at the lazy style in the singleton pattern:
public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }}Copy the code
We all know this is wrong because it doesn’t guarantee thread safety. The optimization is as follows:
public class Singleton { private static Singleton singleton; private Singleton(){} public static synchronized Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }}Copy the code
Optimization is very simple, that is, synchronization is performed on getInstance method, but synchronization will cause this method to be relatively inefficient, resulting in performance degradation, so how to solve it? Smart people came up with the idea of double-checking DCL:
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized (Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
Copy the code
As shown above, this code looks perfect for the following reasons:
- If the first singleton is not null, the following locking action is not required, greatly improving the performance of the program.
- If the first singleton is null, only one thread can create an object because of synchronized, even if there are multiple threads that judge at the same time.
- When the first thread that acquires the lock completes the creation of the singleton object, the other thread will return the created Singleton object directly after determining that the singleton object will not be null for the second time.
From the above analysis, the DCL does look perfect, but it definitely tells you that this is wrong. There is no problem with the above logic, and the analysis is correct, but there is a problem, so what is the problem? Before answering this question, let’s review the object creation process. Instantiating an object consists of three steps:
- Allocating memory space
- Initialize an object
- Assigns the address of the memory space to the corresponding reference
However, due to reordering, reordering may occur in steps 2 and 3 as follows:
- Allocating memory space
- Assigns the address of the memory space to the corresponding reference
- Initialize an object
A reordering of 2 and 3 would cause the second judgment to fail, Singleton! The singleton object for return is an uninitialized object, as follows:
As shown in the legend above, thread B accesses an uninitialized Singleton object.
From the above elaboration, we can judge that the root cause of DCL’s error lies in Step 4:
singleton = new Singleton();
Copy the code
If you know the root of the problem, how can you solve it? There are two solutions:
- Reordering of steps 2 and 3 in the initialization phase is not allowed.
- Reordering of initialization steps 2 and 3 is allowed, but other threads are not allowed to “see” the reordering.
The solution
The solution is based on the above two solutions.
Volatile based solutions
We need to make only a few changes to the DCL above: Make the singleton life volatile:
Public class Singleton {private volatile static Singleton; private volatile static Singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; }}Copy the code
Once the Singleton is declared volatile, steps 2 and 3 will not be reordered and the above problem will be resolved.
Class initialization-based solution
The essence of this solution is to use the classloder mechanism to ensure that instance is initialized with only one thread. The JVM acquires a lock during class initialization that can synchronize multiple threads initializing the same class.
public class Singleton { private static class SingletonHolder{ public static Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.singleton; }}Copy the code
The essence of this solution is to run step 2 and Step 3 reorder, but not allow other threads to see it.
The Java language specifies that for each class or interface C, there is a unique initialization lock LC corresponding to it. The mapping from C to LC is left up to the JVM’s concrete implementation. The JVM acquires this initialization lock during the class initialization phase, and each thread acquires the lock at least once to ensure that the class has been initialized.
The resources
- Fang Tengfei: The Art of Java Concurrent Programming