[Concurrent Topic] Thread Safety of Singleton Mode (Advanced Understanding)

background

After studying JVM recently, I always feel that the knowledge is not deep enough, so I want to deepen my understanding by analyzing the classic [lazy singleton]. (Mainly the way [static inner class] implements a singleton).
If Xiaobai wants to understand singletons, you can also read my article. I also passed [pre-knowledge] and [ordinary lazy style], [double check lock lazy], [static inner class] lazy to analyze their thread safety for everyone. However, I don't have a complete evolution [lazy singleton] process here. Therefore, there will be a lack of progressive thinking. However, in the final [Thanks] list, I provided a complete [Lazy Singleton Evolution] link, and I suggest that you can learn together with this article.

Pre-knowledge

The whole process of class loading and running

When we use the java command to run the main function of a class to start the program, we first need to load the main class into the JVM through the class loader.

package com.tuling.jvm;

public class Math {
    
    
    public static final int initData = 666;
    public static User user = new User();

    public int compute() {
    
      //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
    
    
        Math math = new Math();
        math.compute();
    }
}

The general process of executing code through Java commands is as follows:
insert image description here
The class loading process of loadClass has the following steps:
Load >> Verify >> Prepare >> Analyze >> Initialize >> Use >> Unload

  • Loading: Find the bytecode file on the hard disk and read it through IO. It will be loaded when the class is used, such as calling the main() method of the class, new object, etc., and a representative of this class will be generated in the memory during the loading phase. The java.lang.Class object serves as the access entry for various data of this class in the method area
  • Verification: Verify the correctness of the bytecode file
  • Preparation: allocate memory to static variables of the class and assign default values
  • Analysis: Replace symbolic references with direct references. At this stage, some static methods (symbolic references, such as the main() method) will be replaced with pointers or handles to the memory where the data is stored (direct references). This is the so-called static link process. (Completed during class loading), dynamic linking is completed during the running of the program to replace symbolic references with direct references, the next lesson will talk about dynamic linking
  • Initialization: Initialize the static variables of the class to the specified value and execute the static code block
    insert image description here

To sum up, the above saidLoad >> Verify >> Prepare >> Parse >> InitializeThe process is carried out by the JVM for us, so for us programmers, [innate] has thread safety (this is guaranteed by the JVM for us, and we don't need to care about it).

Implementation of the singleton pattern

The singleton pattern is a very common design pattern in our Java. So there is such a saying: in case of a problem, it is not resolved, but a single case is resolved.
There are usually two types of Java singletons, namely: hungry man style and lazy man style

1. Hungry Chinese style

basic introduction

Hungry Chinese style (Eager Initialization, eager initialization), create a singleton instance when the class is loaded, and return the instance directly when needed. The implementation of this method is thread-safe, because the instance has already been created during the class loading process.

source code

public class SingletonTest {
    
    
    private static final SingletonTest me = new SingletonTest();
    
    public static SingletonTest me() {
    
    
        return me;
    }
    
    public static void main(String[] args) {
    
    
        System.out.println(SingletonTest.me());
        System.out.println(SingletonTest.me());
        System.out.println(SingletonTest.me());
    }
//    系统输出如下:
//    org.tuling.juc.singleton.SingletonTest@12a3a380
//    org.tuling.juc.singleton.SingletonTest@12a3a380
//    org.tuling.juc.singleton.SingletonTest@12a3a380
}

analyze

Because the singleton object SingletonTest is a static member variable, during the JVM class loading process == (Loading-"Verification-"Preparation-"Analysis-"Initialization) the [Analysis] stage of == has been initialized by the JVM, so, by JVM guarantees thread safety.

Two, the lazy style

basic introduction

Lazy Initialization, which creates a singleton instance at the first call, has thread safety issues. If multiple threads enter the judgment condition at the same time, multiple instances may be created.

source code

public class SingletonTest {
    
    
    private static SingletonTest me;

    public static SingletonTest me() {
    
    
        if(me == null) {
    
    
            me = new SingletonTest();
        }
        return me;
    }

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

The output is as follows
insert image description here

analyze

Why is the above code not thread-safe? Let's take an extreme example, as shown in the figure below:
insert image description here
In the absence of a lock mechanism, the above-mentioned concurrent execution may occur in a multi-threaded environment. After 线程1the judgment is completed and before me == nullthe execution is about to start new, the judgment is 线程2just in progress me == null. This is because the operation 线程1has not yet been executed new, so 线程2the judgment must be yes null, so it also starts new. This is where the thread safety issue comes in.
(PS: Xiaobai must understand the above picture. Although it is very simple, it is not an exaggeration to say that it is your step forward, or the enlightenment of cultivating [concurrency awareness].)

Improve

In order to solve the above problems, the big cows have made improvements, using the [double check lock + volatile] mechanism, [double check lock], that is: double check lock. code show as below:

public class SingletonTest {
    
    
    private static volatile SingletonTest me;

    public static SingletonTest me() {
    
    
        if(me == null) {
    
    
            synchronized (SingletonTest.class) {
    
    
                if (me == null) {
    
    
                    me = new SingletonTest();
                }
            }
        }
        return me;
    }
}

The above improvements, the key points are as follows:

  1. volatileThe singleton object is decorated with keywordsme
  2. When obtaining a singleton object, it is judged twiceif(me == null)
  3. Before the second judgment if(me == null), the lock is added

I won’t talk about the second and third points, you can take a look at the [Thanks] friendship link at the bottom. Here we focus on the first point.
It is estimated that it will be difficult for Xiaobai to understand, why must volatilekeyword modification be necessary? The answer is: no. Because, volatilereordering can be prohibited. What is [reordering]? To put it simply, the JVM, or even the CPU, may modify the execution order of our code without changing the semantics for performance. For example, when we new SingletonTest()were talking, you thought there was only one step of operation, but in fact, it has 3 steps, as follows:

memory = allocate(); // 1. Allocate object memory space
instance(memory); // 2. Initialize object
instance = memory; // 3. Set instance to point to the memory address just allocated, at this time instance! =null

But in fact, after reordering, it may become the following execution order:

memory = allocate(); // 1. Allocate object memory space
instance = memory; // 3. Set instance to point to the memory address just allocated, at this time instance! =null
instance(memory); // 2. Initialize the object

Then everyone uses the [Concurrent Enlightenment] consciousness above to draw a picture for yourself. Is it still thread-safe?
Therefore, you need to use volatilekeywords to tell the underlying JVM or CPU, don't help me reorder this object! So the above concurrent thread safety problem is avoided.

3. Lazy-style singleton ultimate solution (static inner class) (recommended use)

basic introduction

Here, by using the feature of JVM class loading [inherently thread-safe], it helps to realize the [lazy style] singleton. How to do it? The answer is [static inner class].

source code

public class SingletonTest {
    
    
    /** 单例对象,可以直接调用配置属性  */
    private static class Holder {
    
    
        private static SingletonTest me = new SingletonTest();
    }
    public static SingletonTest me() {
    
    
        return Holder.me;
    }

    public static void main(String[] args) {
    
    
        int threadCount = 10000;
        for (int i = 0; i < threadCount; i++) {
    
    
            new Thread(()->{
    
    
                System.out.println(SingletonTest.me());
            }).start();
        }
    }
}

In the above code, 10,000 new threads were created to call the singleton. We found that the results are the same, the same object.
insert image description here

analyze

Why above, thread safety can be guaranteed through static inner classes? We have already said this in [Pre-knowledge], the thread safety is guaranteed by the JVM.
insert image description here
As shown in the figure above, only when we use it SingletonTest.me()will we start loading Holderstatic inner classes, which is why it implements [lazy style] (lazy loading).

grateful

Thanks [Author: weixin_47196090] for the in-depth and good article, " Comprehensive Analysis of the Evolution of Lazy Singleton to DCL Lazy "

Guess you like

Origin blog.csdn.net/qq_32681589/article/details/132078283