头图

In-depth understanding of the singleton design pattern

惜鸟
中文

I. Overview

Singleton mode is a question that is often asked in interviews. There are a large number of articles on the Internet that introduce the realization of singleton mode. This article also refers to those excellent articles to make a summary and record through my own understanding in the learning process. And supplement and improve some content, on the one hand to consolidate what I have learned, on the other hand, I hope to provide some help to other students.

This article mainly introduces the singleton mode from the following aspects:

  1. What is the
  2. Usage scenario of
  3. and disadvantages of
  4. singleton mode (emphasis)
  5. summary

Second, what is the singleton pattern

The 23 design patterns can be divided into three categories: creational patterns, behavioral patterns, and structural patterns. The singleton mode is one of the creational modes. The singleton mode is one of the simplest design modes: singleton mode only involves one class, ensuring that there is only one instance of a class in the system, and providing a global access entry . In many cases, the entire system only needs to have one global object, which will help us coordinate the overall behavior of the system.

Third, the use scenario of singleton mode

1. Log type

The log class is usually implemented as a singleton and provides a global log access point in all application components, without the need to create an object each time a log operation is performed.

2. Configuration class

The configuration class is designed as a singleton implementation. For example, in a server program, the configuration information of the server is stored in a file. These configuration data are uniformly read by a singleton object, and then other objects in the service process pass this Singleton objects obtain these configuration information, which simplifies configuration management in complex environments.

3. Factory

Suppose we design an application with a factory to generate new objects with IDs (Acount, Customer, Site, Address objects) in a multithreaded environment. If the factory is instantiated twice in 2 different threads, then 2 different objects may have 2 overlapping ids. If we implement the factory as a singleton, we can avoid this problem. It is a common practice to combine abstract factories or factory methods and singleton design patterns.

accessing resources in shared mode

For example, the counter of a website is generally implemented in a singleton mode. If you have multiple counters, the value of the counter will be refreshed for each user's visit. In this way, the value of your actual count is difficult to synchronize. But if the singleton mode is adopted, there will not be such a problem, and thread safety problems can also be avoided.

5. Bean instances created in Spring all exist in singleton mode by default.

applicable scenarios:

  • An environment that needs to generate a unique sequence
  • Objects that need to be instantiated frequently and then destroyed.
  • Objects that consume too much time or resources when creating objects, but are frequently used.
  • An environment that facilitates communication between resources

Fourth, the advantages and disadvantages of the singleton model

Advantages:

  • There is only one object in memory, saving memory space;
  • Avoid frequent creation and destruction of objects, reduce GC work, and improve performance at the same time;
  • Avoid multiple occupation of shared resources and simplify access;
  • Provide a global access point for the entire system.

Disadvantages:

  • Not suitable for objects that change frequently;
  • Abusing singletons will bring some negative problems. For example, in order to save resources, the database connection pool object is designed as a singleton class, which may lead to too many programs sharing connection pool objects and connection pool overflow;
  • If the instantiated object is not used for a long time, the system will consider the object to be garbage and be recycled, which may cause the loss of the object state;

Fifth, the realization of the singleton mode (emphasis)

The steps to implement the singleton pattern are as follows:

  1. privatization construction method, avoiding external classes to create objects
  2. defines a private static variable to hold its own type
  3. provides a static public method to obtain the instance
  4. If the serialization interface is implemented, it is necessary to ensure that deserialization will not recreate the object

1. Hungry Chinese style, thread safe

The hungry Chinese singleton mode, as the name implies, creates an object as soon as a class is loaded. This method is commonly used, but it is easy to generate garbage objects and waste memory space.

Advantages: thread safety, no locks, higher execution efficiency
Disadvantages: It is not lazy loading, it is initialized when the class is loaded, wasting memory space

Lazy loading: create objects when you use them

How does the hungry singleton ensure thread safety? It is based on the class loading mechanism to avoid the synchronization problem of multiple threads, but if the class is loaded by different class loaders, different instances will be created.

code implementation, and use reflection to destroy the singleton:

/**
 * 饿汉式单例测试
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){}
    // 2、定义一个静态变量指向自己类型
    private final static Singleton instance = new Singleton();
    // 3、对外提供一个公共的方法获取实例
    public static Singleton getInstance() {
        return instance;
    }

}

Use reflection to destroy the singleton, the code is as follows:


public class Test {

    public static void main(String[] args) throws Exception{
        // 使用反射破坏单例
        // 获取空参构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射创建的实例" + singleton);
        System.out.println("正常创建的实例" + Singleton.getInstance());
        System.out.println("正常创建的实例" + Singleton.getInstance());
    }
}

The output is as follows:

反射创建的实例com.example.spring.demo.single.Singleton@6267c3bb
正常创建的实例com.example.spring.demo.single.Singleton@533ddba
正常创建的实例com.example.spring.demo.single.Singleton@533ddba

2. Lazy man, thread is not safe

There is no problem in using this method under single thread. Singleton cannot be guaranteed for multithreading. It is listed here for comparison with the singleton that uses locks to ensure thread safety.

Advantages: lazy loading

Disadvantage: thread is not safe

The code is implemented as follows:


/**
 * 懒汉式单例,线程不安全
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){ }
    // 2、定义一个静态变量指向自己类型
    private static Singleton instance;
    // 3、对外提供一个公共的方法获取实例
    public static Singleton getInstance() {
        // 判断为 null 的时候再创建对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Use multithreading to destroy a singleton, the test code is as follows:

public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("多线程创建的单例:" + Singleton.getInstance());
            }).start();
        }
    }
}

The output is as follows:

多线程创建的单例:com.example.spring.demo.single.Singleton@18396bd5
多线程创建的单例:com.example.spring.demo.single.Singleton@7f23db98
多线程创建的单例:com.example.spring.demo.single.Singleton@5000d44

3. Lazy man, thread safe

How does the lazy singleton ensure thread safety? The synchronized keyword is used to lock to ensure thread safety. synchronized can be added to the method or the code block. The demonstration is added to the method. The problem is that needs to be locked and released every time getInstance is called to obtain an instance. Lock, this is very impactful on performance.

Advantages: lazy loading, thread safe

Disadvantages: low efficiency

The code is implemented as follows:

/**
 * 懒汉式单例,方法上面添加 synchronized 保证线程安全
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){ }
    // 2、定义一个静态变量指向自己类型
    private static Singleton instance;
    // 3、对外提供一个公共的方法获取实例
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4. Double-checked locking (DCL, namely double-checked locking)

The implementation code is as follows:


/**
 * 双重检查锁(DCL, 即 double-checked locking)
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造方法
    private Singleton() {
    }

    // 2、定义一个静态变量指向自己类型
    private volatile static Singleton instance;

    // 3、对外提供一个公共的方法获取实例
    public synchronized static Singleton getInstance() {
        // 第一重检查是否为 null
        if (instance == null) {
            // 使用 synchronized 加锁
            synchronized (Singleton.class) {
                // 第二重检查是否为 null
                if (instance == null) {
                    // new 关键字创建对象不是原子操作
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Advantages: lazy loading, thread safety, high efficiency

Disadvantages: more complicated to implement

The double check here refers to two non-empty judgments, and the lock refers to synchronized locking. Why double judgments are actually very simple. The first judgment is that if the instance already exists, then there is no need to perform synchronization operations. It is to return to this instance directly. If it is not created, it will enter the synchronization block. The purpose of the synchronization block is the same as before. The purpose is to prevent multiple instances from being called at the same time by multiple threads. There can be a thread call to access the contents of the synchronized block. When the first call to grab the lock acquires the instance, the instance will be created. All subsequent calls will not enter the synchronized block, and return directly at the first judgment. A singleton.

Regarding the role of the internal second empty judgment, when multiple threads reach the lock position together, the lock competition is carried out. One of the threads acquires the lock. If it is the first entry, it will be null, and the singleton object will be created and completed. After the lock is released, other threads will be intercepted by the null judgment after acquiring the lock, and directly return the created singleton object.

One of the most important points is the use of the volatile volatile , you can directly search for the volatile keyword. There are many very well-written articles. I will not introduce them in detail here. I will explain briefly. Double check the lock. use volatile two important characteristics: visibility, prohibit instruction reordering

Why use volatile here?

This is because new keyword is not an atomic operation. Creating an object will go through the following steps:

  1. Open up memory space in heap memory
  2. Call the constructor, initialize the object
  3. Reference variable points to heap memory space

The corresponding bytecode instructions are as follows:

image.png

In order to improve performance, compilers and processors often reorder instructions in a given code execution order. From the source code to the final execution of the instructions, the following process will be experienced:

graph LR
A[源码] -->B([编译器优化重排序])-->C([指令级并行重排序])-->D([内存系统重排序])-->E[最终执行指令序列]

So after instruction reordering, the execution order of the created object may be 1 2 3 or 1 3 2 . Therefore, when a thread runs the 1 3 2 instruction out of order, the reference variable points to the heap memory space. This object is not null, but it is not initialized. Others The thread may enter the first if(instance == null) getInstance at this time, which is judged not to be null, which leads to the incorrect use of non-null instances that are not initialized. In this case, an exception will occur. This is the famous DCL failure problem.

When we add the volatile keyword to the reference variable, we will add a memory barrier before and after the creation of the object instruction to prohibit instruction reordering. This problem can be avoided, and the volatile is visible to any other thread. of.

5. Static inner class

The code is implemented as follows:


/**
 * 静态内部类实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造方法
    private Singleton() {
    }
    
    // 2、对外提供获取实例的公共方法
    public static Singleton getInstance() {
        return InnerClass.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerClass{
        private final static Singleton INSTANCE = new Singleton();
    }

}

Advantages: lazy loading, thread safety, high efficiency, simple implementation

How does static inner class singleton realize lazy loading? First of all, we first understand the loading timing of the next class.

The virtual machine specification requires that there are only 5 cases where the class must be initialized immediately (loading, verification, and preparation need to start before this):

  1. encounters the 4 bytecode instructions new , getstatic , putstatic , invokestatic The most common Java code scenarios for generating these 4 instructions are: when using the new keyword to instantiate an object, reading or setting a static field of a class (except for final modification, the static field modified by final is a constant, which is already at compile time When putting the result into the constant pool), and when calling a static method of a class.
  2. uses the java.lang.reflect package method to make a reflection call to the class.
  3. When initializing a class, if it is found that the parent class has not been initialized, the initialization of the parent class needs to be triggered first.
  4. When the virtual machine starts, the user needs to specify a main class to be executed (the class containing main()), and the virtual machine initializes this main class first.
  5. When using the dynamic language support of JDK 1.7, if the final analysis result of java.lang.invoke.MethodHandle instance is the method handle of REF_getStatic, REF_putStatic , REF_invokeStatic , the initialization of the class corresponding to this method handle needs to be triggered first.

These 5 cases are called active references of classes. Note that the qualifier used in the "Virtual Machine Specification" is " and only ". Then, all other referenced classes will not be affected The class is initialized, which is called passive reference. A static inner class is a case of passive reference.

When the getInstance() method is called, InnerClass is in the runtime constant pool of Singleton, replacing the symbolic reference with a direct reference. At this time, the static object INSTANCE is actually created, and then it is returned by the getInstance() method. Same as the hungry man mode.

So how does INSTANCE ensure thread safety during the creation process? In "In-depth understanding of JAVA virtual machine", there is such a sentence:

The virtual machine guarantees that the <clinit>() method of a class is correctly locked and synchronized in a multithreaded environment. If multiple threads initialize a class at the same time, only one thread will execute the <clinit>() method of this class, and other threads need to be blocked. Wait until the active thread <clinit>() method. If there is a long time-consuming operation in the <clinit>() method of a class, it may cause multiple processes to be blocked ( should be noted that although other threads will be blocked, if the <clinit>() method is executed, the other threads will not wake up. Enter the <clinit>() method again. Under the same loader, a type will only be initialized once. ). In practical applications, this kind of blocking is often very hidden.

From the above analysis, it can be seen that INSTANCE is thread-safe during the creation process, so the singleton in the form of a static inner class can guarantee thread safety and the uniqueness of the singleton, while also delaying the instantiation of the singleton.

6. Enumeration singleton

The code is implemented as follows:

/**
 * 枚举实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public enum Singleton {
    INSTANCE;
    public void doSomething(String str) {
        System.out.println(str);
    }
}

Advantages: simple, efficient, thread-safe, can avoid destroying the enumeration singleton through reflection

Enumerations in Java can have fields and methods just like ordinary classes, and the creation of enumeration instances is thread-safe. In any case, it is a singleton and can be called directly to obtain the instance in the following way:

Singleton singleton = Singleton.INSTANCE;

Use the following command to decompile the enumeration class

javap Singleton.class

Get the following

Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
  public static final com.spring.demo.singleton.Singleton INSTANCE;
  public static com.spring.demo.singleton.Singleton[] values();
  public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
  public void doSomething(java.lang.String);
  static {};
}

The results can be seen from decompiling enumeration, INSTANCE is static final modified, it can be called directly by the class name, and create an instance of an object is created in the static code block , because the static type of property in the class is loaded After it is initialized, when a Java class is actually used for the first time, the static resources are initialized, the loading and initialization of the Java class are thread-safe, so creating an enum type is thread-safe.

The enumeration is destroyed by reflection, and the implementation code is as follows:

public class Test {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething("hello enum");

        // 尝试使用反射破坏单例
        // 枚举类没有空参构造方法,反编译后可以看到枚举有一个两个参数的构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例,这里会报错,因为无法通过反射创建枚举的实例
        Singleton enumSingleton = declaredConstructor.newInstance();
        System.out.println(enumSingleton);
    }
}

The operation result reported the following error:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at com.spring.demo.singleton.Test.main(Test.java:24)

newInstance() method of the reflection creation instance, there are the following judgments:

image.png

Therefore, it is not possible to create an instance of an enumeration through reflection.

Six, summary

In Java, if a Singleton class implements the java.io.Serializable interface, when the singleton is serialized and then deserialized multiple times, multiple instances of the Singleton class will be created. To avoid this situation, the readResolve method should be implemented. Please refer to Serializable () and readResolve Method () in javadocs.


public class Singleton implements Serializable {
    // 1、私有化构造方法
    private Singleton() {
    }

    // 2、对外提供获取实例的公共方法
    public static Singleton getInstance() {
        return InnerClass.instance;
    }

    // 定义静态内部类
    private static class InnerClass{
        private final static Singleton instance = new Singleton();
    }


    // 对象被反序列化之后,这个方法立即被调用,我们重写这个方法返回单例对象.
    protected Object readResolve() {
            return getInstance();
    }
}

to note when using the singleton design pattern:

  • Multithreading-You should be especially careful when singletons must be used in a multithreaded application.
  • Serialization-When singletons implement the Serializable interface, they must implement the readResolve method to avoid two different objects .
  • Class Loader-If the Singleton class is loaded by 2 different class loaders, we will have 2 different classes, one for each class.
  • The global access point represented by the class name-use the class name to get the singleton instance. This is an easy way to access it, but it is not very flexible. If we need to replace the Sigleton class, all references in the code should be changed accordingly.

This article briefly introduces several implementations of the singleton design pattern. In addition to the enumeration singleton, all other implementations can destroy the singleton pattern through reflection. In "effective java", it is recommended that the enumeration implement the singleton mode. In actual scenarios Which singleton implementation to use in, you need to choose according to your own situation, and the best way is suitable for the current scenario.

Reference article

https://blog.csdn.net/mnb65482/article/details/80458571

https://www.oodesign.com/singleton-pattern.html

阅读 701
198 声望
2.2k 粉丝
0 条评论
你知道吗?

198 声望
2.2k 粉丝
宣传栏