Singleton mode of DCL

The so-called DCL is Double Check Lock, that is, double lock check. Before understanding how DCL is applied in singleton mode, let's first understand the singleton mode. Singleton mode is usually divided into "hungry man" and "lazy man", start with simple

Hungry man

The so-called "hungry man" is because the instance is created when the program is started. In layman's terms, it means that the food is just served, and everyone eats a bite before they start eating.

public class Singleton {
    
    
    private static final Singleton singleton = new Singleton();
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        return singleton;
    }
}

Line 3 restricts the way to create such objects through a private constructor (ignored by reflection). This method is very safe, but it is a waste of resources to some extent. For example, a Singleton instance is created from the beginning, but it is rarely used. This results in a waste of resources in the method area. Therefore, another method appears. Singleton mode, the lazy singleton mode

Lazy man

The reason why it is called a "lazy man" is because it only appears when it is really called it. If it is not called it will ignore it, and it has nothing to do with it. In other words, the instance is created when it is actually used, not at the beginning. As shown in the following code:


public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            singleton = new Singleton();
        }
        return singleton;
    }
}

It seems a very simple piece of code, but there is a problem, that is, thread insecurity. For example, now there are 1000 threads, all of which need this instance of Singleton, to verify whether the same instance is obtained, the code is as follows:

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            try {
    
    
                Thread.sleep(1);//象征性的睡了1ms
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Part of the running results are messy:

944436457
1638599176
710946821
67862359

Why is this happening? The first thread came over, executed to line 7, and slept for 1ms. While sleeping, the second thread came. When the second thread executes to line 5, the result must be empty, so there will be Two threads each create an object, which will inevitably lead to Singleton.getInstance().hashCode()inconsistent results. It can be improved as follows by adding a lock to the entire method:

Improvement 1

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static synchronized Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            try {
    
    
                Thread.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

The thread consistency problem is solved by adding synchronized to the getInstance() method. The result analysis shows that the hashcodes of all instances are the same, but the granularity of synchronized is too large, that is, the critical area of ​​the lock is too large, which affects efficiency a little, for example, if There is business processing logic between lines 4 and 5, which will not involve shared variables, so every time this part of the business logic is locked, it will inevitably lead to inefficiency. In order to solve the coarse-grained problem, the code can be further improved:

Improvement 2

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                try {
    
    
                    Thread.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                singleton = new Singleton();
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Part of the running results:

391918859
391918859
391918859
1945023194

By analyzing the running results, it is found that although the granularity of the lock has become smaller, the thread is not safe. Why is this so? Because there is a situation, the time slice is used up when thread 1 finishes executing the if judgment and has not obtained the lock. At this time, thread 2 comes. When executing the if judgment, it is found that the object is still empty. Continue to execute the execution and get it smoothly. The lock is reached, so thread 2 creates an object. When thread 2 is created, the lock is released. At this time, thread 1 is activated, successfully obtains the lock, and creates an object. So the code needs another step of improvement.

Improvement 3

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                if(null == singleton){
    
    //DCL
                    try {
    
    
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

By adding another layer of if judgment on line 10, it is the so-called Double Check Lock. That is to say, even if you get the lock, you have to make a judgment. If the judgment object is not empty at this time, then you don’t need to create the object again and just return directly. This solves the problem in "Improvement 2". problem. But whether you can go to the 8th line here, I personally think it is fine. If you keep the 8th line, it is to improve efficiency, because if you go, each thread will directly grab the lock when it comes over. The lock grab itself will affect the efficiency. The if judgment is only a few ns, and most threads do not need to grab the lock, so it is best to keep it.
The principle of DCL singleton is now introduced, but there is still a problem. It is necessary to consider the issue of instruction reordering, so volatile must be added to prohibit instruction reordering. Continue to analyze the code, and simplify the Singleton code for analysis convenience:

public class Singleton {
    
    
    int a = 5;//考虑指令重排序的问题
}

singleton = new Singleton()The bytecode is as follows:

  0: new    #2           // class com/reasearch/Singleton
  3: dup
  4: invokespecial #3   // Method com/reasearch/Singleton."<init>":()V
  7: astore_1

Regardless of the dup command. Add a knowledge point here. When creating an object, allocate space first. Variables in the class have a default value, and then assign values ​​to variables after calling the constructor. For example int a = 5, a=0 at the beginning. The execution process of bytecode instructions is as follows,

  1. new allocates space, a=0
  2. invokespecial construction method a=5
  3. astore_1 assigns the object to singleton

This is the ideal state. 2 and 3 have no semantic and logical connection, so JVM can allow these instructions to be executed out of order, that is, execute 3 first and then execute 2. Going back to improvement 3 , if thread 1 executes the 16th line of code again, the order of execution of instructions is 1, 3, 2. When 3 is executed, the time slice is used up, at this time a=0, which means that the initialization is halfway. When it hangs. At this time, thread 2 is coming, and the eighth line judges that the singleton is definitely not empty, so it directly returns a Singleton object, but in fact this object is a problem object, a semi-initialized object, ie a=0. This is caused by instruction reordering, so in order to prevent this phenomenon, add the keyword volatile. Therefore, the final version of the code of the singleton mode of DCL is as follows:

full version

public class Singleton {
    
    
    private volatile static Singleton singleton = null;//加上volatile 
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                if(null == singleton){
    
    //DCL
                    try {
    
    
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

At this point, we can come to an end. I believe many small partners will write singletons, but it is still difficult to understand the principles. Let's cheer!

Guess you like

Origin blog.csdn.net/hongyinanhai00/article/details/113971787