63-Why do you have to add volatile to the double check lock mode of singleton mode?

What is the singleton pattern

The singleton mode refers to ensuring that there is only one instance of a class and providing an entry that can be accessed globally.

Why do we need to use the singleton pattern

So why do we need singletons? One of the most important reasons is to save memory and save calculations. **Because in many cases, we only need one instance. If there are more instances, it will be a waste.

Let's give an example to illustrate this situation. For a class that is time-consuming to initialize, the code is as follows:

public class ExpensiveResource {
    
    
    public ExpensiveResource() {
    
    
        field1 = // 查询数据库
        field2 = // 然后对查到的数据做大量计算
        field3 = // 加密、压缩等耗时操作
    }
}

When this class is constructed, it needs to query the database and do a lot of calculations on the data found, so we spent a lot of time initializing this object when it was first constructed. But assuming that the data in the database is unchanged, we can store this object in memory, so that the same instance can be used directly in future development, without the need to build a new instance again. If a new instance is regenerated every time, it will cause more waste, which is really unnecessary.

Next, let's look at the second reason for singletons, which is to ensure the correct results. **For example, we need a global counter to count the number of people. If there are multiple instances, it will cause confusion.

In addition, it is for the convenience of management. **For many tool classes, we only need one instance, so it is very convenient for us to obtain this singleton through a unified entry, such as the getInstance method. Too many instances are not only not helpful, but dazzling.

The general class structure of the singleton mode is shown in the following figure: there is a private singleton object of type Singleton; at the same time, the constructor is also private, in order to prevent others from calling the constructor to generate an instance; there will also be a public getInstance method, which can be The singleton is obtained by this method.
Insert picture description here

How to write double check lock mode

There are many ways to write the singleton mode. Let's focus on the way to write the double check lock mode that is strongly related to volatile. The code is as follows:

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

Here I will focus on the getInstance method. In the method, first an if (singleton == null) check is performed, then a synchronized block, then another if (singleton == null) check, and finally singleton = new Singleton() to generate an instance.

We performed two if (singleton == null) checks, which is the origin of the name "double check lock". This way of writing can ensure thread safety. Assuming that two threads reach the synchronized statement block at the same time, the instantiation code will only be executed once by the thread that grabs the lock first, and then the thread that grabs the lock will be in the second if In the judgment, it is found that singleton is not null, so the statement to create an instance is skipped. When other threads later call the getInstance method, they only need to judge the first if (singleton == null), and then skip the entire if block and directly return the instantiated object.

The advantage of this writing method is not only thread safety, but also lazy loading and higher efficiency.

Speaking of this, a common question is involved. The interviewer may ask you, "Why double-check? Is it okay to remove any check?"

Let’s look at the second check first. At this time, you need to consider a situation where two threads call the getInstance method at the same time. Since the singleton is empty, both threads can pass the first if judgment; Due to the existence of the lock mechanism, one thread will enter the synchronization statement first and enter the second if judgment, while another thread will wait outside.

However, when the first thread executes the new Singleton() statement, it will exit the synchronized protected area. At this time, if there is no second if (singleton == null) judgment, then the second thread will also create one Instance, the singleton is destroyed at this time, which is definitely not feasible.

For the first check, if it is removed, all threads will be executed serially, which is inefficient, so both checks need to be retained.

Why do you need to use the volatile keyword in the double-checked lock mode

I believe you may have seen that if you are careful, we have added the volatile keyword to the singleton object in the double-checked lock mode, so why use volatile? **The main reason lies in singleton = new Singleton(), which is not an atomic operation. In fact, the above statement in JVM does at least the following three things:

Insert picture description here

  • The first step is to allocate memory space for singleton;
  • Then the second step starts to call the constructor of Singleton, etc., to initialize the singleton;
  • Finally, the third step is to point the singleton object to the allocated memory space (singleton is not null after this step is executed).

Here you need to pay attention to the order of 1-2-3, because there is an optimization of instruction reordering, which means that the order of steps 2 and 3 cannot be guaranteed. The final execution order may be 1-2-3. It may also be 1-3-2.

If it is 1-3-2, then after step 3 is executed, the singleton is not null, but step 2 is not executed at this time, the singleton object has not been initialized, and the value of its property may not be what we expected value. Assuming that thread 2 enters the getInstance method at this time, since the singleton is no longer null, it will pass the first check and return directly, but in fact, the singleton at this time has not been initialized, so an error will be reported when using this instance. The detailed process is as follows As shown in the figure:
Insert picture description here
Thread 1 first executes the first step of creating a new instance, that is, allocating the memory space of a singleton object. Because thread 1 is reordered, it executes the third step of creating a new instance, which is to allocate singleton to the previous one. After the execution of this third step, the singleton object is no longer null.

At this time, thread 2 enters the getInstance method and judges that the singleton object is not null. Then thread 2 returns and uses the singleton object. Since it is not initialized, an error is reported. Finally, thread 1 "comes late" before it starts to execute the second step of creating a new instance-initializing the object, but at this time the initialization is too late because an error has already been reported.

After volatile is used, it is equivalent to indicating that the update of the field may occur in other threads, so it should be ensured that when reading the value written by another thread, the next required operation can be performed smoothly. In the JMM used in JDK 5 and subsequent versions, after volatile is used, the reordering of related statements is prohibited to a certain extent, thus avoiding the above-mentioned problem of reading incomplete objects due to reordering.

This concludes the question of "why use volatile". The main point of using volatile is that it can prevent objects that have not been initialized, thus ensuring thread safety.

to sum up

In this article, we first introduced what singleton mode is and why we need to use singleton mode. Then we introduced double-check lock mode, why double-check is needed when faced with this kind of writing, and why do we need volatile? The most important thing is to ensure thread safety.

Guess you like

Origin blog.csdn.net/Rinvay_Cui/article/details/111059024