Getting Started with Singleton Pattern

I believe everyone has heard of the singleton pattern, and has even written about it a lot. It is also one of the most tested design patterns in interviews. Interviewers often ask to write down two types of singleton patterns and explain their principles. Without further ado, let’s start learning how to answer this interview question well.

1. What is the singleton pattern?

When the interviewer asks what is the singleton pattern, do not answer the question incorrectly. Give an answer such as there are two types of singleton patterns, and focus on the definition of the singleton pattern.

The singleton pattern refers to a design pattern that creates objects in memory only once. When the same object is used multiple times in a program with the same function, in order to prevent frequent creation of objects that cause the memory to soar, the singleton mode allows the program to create only one object in the memory , so that all places that need to be called share this singleton. object.

Insert image description here

2. Types of singleton pattern

There are two types of singleton pattern:

  • Lazy style: Create the singleton class object only when you really need to use the object
  • Hungry Chinese style: The singleton object has been created when the class is loaded and is waiting to be used by the program.
Create a singleton object in a lazy way

The lazy way to create an object is to determine whether the object has been instantiated (empty test) before the program uses the object. If it has been instantiated, it will directly return the object of this class. Otherwise, the instantiation operation is performed first.

Insert image description here
According to the above flow chart, you can write the following code

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

Yes, we have written a very good singleton pattern here, but it is not perfect, but this does not affect our use of this "singleton object".

The above is the lazy way to create a singleton object. I will explain later where this code can be optimized and what problems there are.

Create singleton object in Hungry style

Hungry Style has already created the object when the class is loaded , and can directly return the singleton object when the program is called. That is, we have already specified that we want to create this object immediately when coding, and we do not need to wait until it is called. .

Regarding class loading, when it comes to JVM content, we can currently simply think that this singleton object has been created when the program starts.

Insert image description here

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

Note that the above code has already instantiated a Singleton object in memory in line 3, and there will not be multiple Singleton object instances.

When a class is loaded, a Singleton object is created in the heap memory. When the class is unloaded, the Singleton object also dies.

3. How to ensure that only one object is created in a lazy way

Let’s review the core methods of the lazy man again

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

This method actually has problems. Just imagine, if two threads determine that singleton is empty at the same time, then they will instantiate a Singleton object, which becomes a double instance. Therefore, what we have to solve is the issue of thread safety.

Insert image description here

The easiest solution to think of is to lock the method or lock the class object. The program will look like this:

public static synchronized Singleton getInstance() {
    
    
    if (singleton == null) {
    
    
        singleton = new Singleton();
    }
    return singleton;
}
// 或者
public static Singleton getInstance() {
    
    
    synchronized(Singleton.class) {
    
       
        if (singleton == null) {
    
    
            singleton = new Singleton();
        }
    }
    return singleton;
}

This avoids the risk of two threads creating Singleton objects at the same time, but it leads to another problem: every time you want to obtain an object, you need to acquire the lock first, and the concurrency performance is very poor. In extreme cases, lags may occur .

The next thing to do is to optimize performance . The goal is : if the object has not been instantiated, lock it and create it. If it has been instantiated, there is no need to lock it and get the instance directly.

Therefore, the method of directly adding locks to the method is abandoned, because this method requires acquiring the lock first anyway.

public static Singleton getInstance() {
    
    
    if (singleton == null) {
    
      // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(Singleton.class) {
    
     // 线程A或线程B获得该锁进行初始化
            if (singleton == null) {
    
     // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

The above code has perfectly solved the problem of concurrency safety + performance inefficiency :

  • In the second line of code, if the singleton is not empty, the object is returned directly without acquiring a lock; and if multiple threads find that the singleton is empty, they enter the branch;
  • In the third line of code, multiple threads try to compete for the same lock. Only one thread succeeds. The first thread to obtain the lock will judge again whether the singleton is empty, because the singleton may have been instantiated by a previous thread.
  • When other threads that acquire the lock later execute the verification code on line 4 and find that the singleton is no longer empty, they will not create a new object and just return the object directly.
  • All subsequent threads entering this method will not acquire the lock. The singleton object is no longer empty when it is first judged.

Because it requires two null checks and locks the class object, this lazy writing method is also called: Double Check + Lock

The complete code is as follows:

public class Singleton {
    
    
    
    private static Singleton singleton;
    
    private Singleton(){
    
    }
    
    public static Singleton getInstance() {
    
    
        if (singleton == null) {
    
      // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) {
    
     // 线程A或线程B获得该锁进行初始化
                if (singleton == null) {
    
     // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

The above code is almost perfect, but there is still one last problem: instruction rearrangement

4. Use volatile to prevent instruction reordering

Creating an object goes through three steps in the JVM:

  1. Allocate memory space for singleton
  2. Initialize singleton object
  3. Point the singleton to the allocated memory space

Instruction reordering means: While ensuring that the final result is correct, the JVM can execute statements not in the order of program coding to improve the performance of the program as much as possible.

Among these three steps, instructions may be rearranged in steps 2 and 3, and the order of creating objects becomes 1-3-2, which will cause multiple threads to obtain objects. It is possible that thread A is in the process of creating objects. , after executing steps 1 and 3, thread B determines that the singleton is not empty, obtains an uninitialized singleton object, and reports an NPE exception. The text is rather obscure, you can look at the flow chart:

Insert image description here

The volatile keyword can be used to prevent instruction reordering . The principle is relatively complicated and will not be elaborated on in this blog. It can be understood as follows: variables modified with the volatile keyword can ensure that the order of instruction execution is consistent with the order specified by the program. Sequence transformation will occur so that NPE exceptions will not occur in a multi-threaded environment.

volatile also has a second function: variables modified with the volatile keyword can ensure its memory visibility, that is, the value of the variable read by the thread at each moment is the latest value in the memory, and each time the thread operates on the variable You need to read the variable first.

public class Singleton {
    
    
    
    private static volatile Singleton singleton;
    
    private Singleton(){
    
    }
    
    public static Singleton getInstance() {
    
    
        if (singleton == null) {
    
      // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) {
    
     // 线程A或线程B获得该锁进行初始化
                if (singleton == null) {
    
     // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

5. Destroy lazy-style singletons and hungry-style singletons

Whether it is the perfect lazy style or the hungry style, it is ultimately no match for reflection and serialization. Both of them can destroy singleton objects (generate multiple objects).

1: Use reflection to destroy the singleton mode
public static void main(String[] args) {
    
    
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}

The above code hits the nail on the head: use reflection to force access to the private constructor of the class to create another object.

2: Use serialization and deserialization to destroy the singleton model
public static void main(String[] args) {
    
    
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

The reason why the addresses of the two objects are not equal is that when the readObject() method reads the object, it will definitely return a new object instance, which must point to the new memory address.

6. Enumeration implementation that can make the interviewer applaud

We have mastered the common writing methods of lazy man style and hungry man style, and the singleton mode chapter in "Dahua Design Patterns" stops here. However, how can we, who pursue the ultimate, stop here? In the book "Effective Java", the ultimate solution is given. Not much to say, after learning the following, you will really be worthy of the interviewer's test.

After JDK1.5, there is another way to implement singleton mode using Java language: enumeration

Let’s first take a look at how enumeration implements singleton mode, as shown in the following code:

public enum Singleton {
    
    
    INSTANCE;
    
    public void doSomething() {
    
    
        System.out.println("这是枚举类型的单例模式!");
    }
}

Need to think about: What are the advantages of using enumerations to implement the singleton pattern?

We start from the most intuitive place. When you first see these lines of code, you will feel "less", yes, it is less. Although this advantage is a bit far-fetched, the less code you write, the less likely it is to make mistakes.

Advantage 1 : The code is more concise compared to the Hungry Man style and the Lazy Man style.

Secondly, since we are implementing the singleton mode, this writing method must meet the requirements of the singleton mode , and no additional processing is done when using enumeration to implement it.

Advantage 2 : It does not require any additional operations to ensure object singleness and thread safety

I wrote a piece of test code and put it below. This piece of code can prove that only one Singleton object will be created when the program starts, and it is thread-safe.

We can simply understand the process of enumeration to implement singletons: when the program starts, Singleton's empty parameter constructor will be called, a Singleton object will be instantiated and assigned to INSTANCE, and it will never be instantiated again.

public enum Singleton {
    
    
    INSTANCE;
    Singleton() {
    
     System.out.println("枚举创建对象了"); }
    public static void main(String[] args) {
    
     /* test(); */ }
    public void test() {
    
    
        Singleton t1 = Singleton.INSTANCE;
        Singleton t2 = Singleton.INSTANCE;
        System.out.print("t1和t2的地址是否相同:" + t1 == t2);
    }
}
// 枚举创建对象了
// t1和t2的地址是否相同:true

In addition to advantages 1 and 2, there is also the last advantage that makes enumeration implement singleton mode currently considered "impeccable".

Advantage 3 : Using enumerations can prevent callers from using reflection, serialization and deserialization mechanisms to force the generation of multiple singleton objects and destroy the singleton mode.

The principle of anti-vandalism is as follows:

(1) Anti-reflection

Insert image description here
The enumeration class inherits the Enum class by default. When newInstance() is called using reflection, it will be judged whether the class is an enumeration class. If so, an exception will be thrown.

(2) Prevent deserialization from creating multiple enumeration objects

When reading a Singleton object, each enumeration type and enumeration name is unique, so during serialization, only the enumeration type and variable name are output to the file, and the file is deserialized into an object after reading the file. When, use the valueOf(String name) method of the Enum class to find the corresponding enumeration object based on the name of the variable.

Therefore, during the serialization and deserialization process, only the enumeration type and name are written and read , without any operations on the object.

Insert image description here
Summary:

  • The Enum class uses Enum type determination internally to prevent multiple objects from being created through reflection.
  • The Enum class serializes (deserializes) the object by writing out (reading in) the object type and enumeration name, and finds the only object instance in the memory by matching the enumeration name through the valueOf() method to prevent multiple objects from being constructed through deserialization. objects
  • The enumeration class does not need to pay attention to thread safety, destroying singletons and performance issues, because the timing of creating objects is similar to that of hungry singletons .

7. Summary

  • There are two common ways to write the singleton pattern: lazy man style and hungry man style.
  • Lazy style: Instantiate the object only when the object is needed. The correct implementation method is: Double Check + Lock , which solves the problems of concurrency safety and low performance.
  • Hungry Chinese style: The singleton object has been created when the class is loaded , and the object can be returned directly when obtaining the singleton object. There will be no concurrency security and performance issues.
  • During development, if the memory requirements are very high , you can use lazy writing to create the object at a specific time;
  • If the memory requirements are not high, use Hungry-style writing because it is simple, less error-prone , and does not have any concurrency security and performance issues.
  • In order to prevent variables from reporting NPE due to instruction reordering in a multi-threaded environment, the volatile keyword needs to be added to the singleton object to prevent instruction reordering.
  • The most elegant implementation is to use an enumeration , whose code is streamlined, has no thread safety issues, and the Enum class internally prevents singletons from being destroyed during reflection and deserialization .

Guess you like

Origin blog.csdn.net/weixin_45888036/article/details/132408505