Singleton singleton pattern

The singleton pattern is the easiest to understand among the design patterns, and it is also the easiest pattern to code by hand. But there are many pits, so it is often used as an interview question. This article mainly organizes several singleton writing methods, and analyzes their advantages and disadvantages. A lot of them are clichés, but if you don't know how to create a thread-safe singleton, and don't know what a double-checked lock is, this article might help you.

Lazy, thread-unsafe

When asked to implement a singleton pattern, the first reaction of many people is to write the following code, including textbooks that teach us this.

copy code
public class Singleton {
        private static Singleton instance;

        private Singleton() {
        }

        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
copy code

This code is simple and clear, and uses lazy loading mode, but there are fatal problems. When multiple threads call getInstance() in parallel, multiple instances are created. That is to say, it does not work properly under multi-threading.

Lazy, thread-safe

To solve the above problem, the easiest way is to make the whole getInstance() method synchronized.

copy code
public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
copy code

Although it is thread-safe and solves the problem of multiple instances, it is not efficient. Because only one thread can call the getInstance() method at any time. But the synchronous operation is only needed when it is called for the first time, that is, when the singleton instance object is first created. This leads to double-checked locks.

double check lock

The double checked locking pattern is a method of locking using synchronized blocks. Programmers call this a double-checked lock because there will be two checks  instance == null, one outside the synchronized block and one inside the synchronized block. Why is it checked again within the synchronized block? Because there may be multiple threads entering the if outside the synchronized block together, multiple instances will be generated if the second check is not performed in the synchronized block.

copy code
public static Singleton getSingleton() {    
            if (instance == null) { //First check                     
                synchronized (Singleton.class) {            
                    if (instance == null) { //Second test                   
                            instance = new Singleton();            
                    }    
                }
            }
            return instance ;}
            }
        }
copy code

This code looks perfect, unfortunately it is buggy. Mainly because of instance = new Singleton()this sentence, this is not an atomic operation. In fact, in the JVM, this sentence probably does the following three things.

  1. Allocate memory to instance
  2. Call the constructor of Singleton to initialize member variables
  3. Point the instance object to the allocated memory space (the instance will be non-null after this step)

But there are optimizations for instruction reordering in the JVM's just-in-time compiler. That is to say, the order of the second and third steps above is not guaranteed, and the final execution order may be 1-2-3 or 1-3-2. If it is the latter, it is preempted by thread 2 before 3 is executed and 2 is not executed. At this time, instance is already non-null (but not initialized), so thread 2 will directly return to instance, then use it, and then logically report an error.

We just need to declare the instance variable as volatile.

copy code
public class Singleton {
        private volatile static Singleton instance; // 声明成 volatile

        private Singleton() {
        }

        public static Singleton getSingleton() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
copy code

Some people think that the reason for using volatile is visibility, that is, it can ensure that the thread does not have a copy of the instance locally, and each time it is read from main memory. But it's not right. The main reason for using volatile is another feature: to disable instruction reordering optimizations. That is, there will be a memory barrier (on the generated assembly code) after the assignment to a volatile variable, and reads will not be reordered before the memory barrier. For example, in the above example, the fetch operation must be performed after 1-2-3 or after 1-3-2. There is no situation where 1-3 is executed and then the value is obtained. From the point of view of the "first occurrence principle", it means that the write operation to a volatile variable occurs first before the subsequent read operation of the variable (the "later" here is the order of time).

But pay special attention to the problem of double-checked locking using volatile in versions prior to Java 5. The reason is that the JMM (Java Memory Model) before Java 5 is flawed. Even declaring variables as volatile cannot completely avoid reordering. The main reason is that the code before and after volatile variables still has reordering problems. This volatile-masked reordering problem was fixed in Java 5, so it's safe to use volatile after that.

I believe you will not like this complex and implicit problem, of course, we have a better way to achieve thread-safe singleton pattern.

Hungry-style static final field

This method is very simple, because the instance of the singleton is declared as a static and final variable, which is initialized when the class is loaded into memory for the first time, so the creation of the instance itself is thread-safe.

public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return instance;
}
}

If this way of writing is perfect, there is no need to talk about so many double-checked locks. The disadvantage is that it is not a lazy initialization mode, the singleton will be initialized as soon as the class is loaded, even if the client does not call the getInstance() method. The Hungry-style creation method will not be available in some scenarios: for example, the creation of a Singleton instance depends on parameters or configuration files. Before getInstance(), a method must be called to set parameters for it, so this singleton writing method cannot be used. used.

static nested class

I prefer to use the static inner class method , which is also recommended in Effective Java.

copy code
public class Singleton {
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }

        private Singleton() {
        }

        public static final Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
copy code

This way of writing still uses the JVM's own mechanism to ensure thread safety; since SingletonHolder is private, there is no way to access it except getInstance(), so it is lazy; at the same time, it will not be synchronized when reading the instance, no Performance flaw; also does not depend on JDK version.

enum Enum

Writing singletons with enums is so easy! This is also its greatest advantage. The following code is the usual way to declare an enum instance.

public enum EasySingleton{
INSTANCE;
}

We can access the instance through EasySingleton.INSTANCE, which is much simpler than calling the getInstance() method. Creating an enum is thread-safe by default, so you don't need to worry about double checked locking, and it also prevents deserialization from recreating new objects. But it is still rare to see someone write this, maybe because they are not familiar with it.

Summarize

Generally speaking, there are five ways to write the singleton pattern: lazy, hungry, double-checked lock, static inner class, and enumeration. The above are all thread-safe implementations. The first method given at the beginning of the article is not correct.

As far as I am concerned, in general, it is fine to use the hungry Chinese style directly. If lazy initialization is explicitly required, I will tend to use static inner classes. If it involves deserialization to create objects, I will try to use enumeration. way to implement a singleton.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325207942&siteId=291194637