Singleton design pattern - Enumeration singleton - singleton optimization

preface

Singleton mode is the simplest of Java design modes. You only need one class to implement singleton mode. However, you can't underestimate singleton mode. Although it is relatively simple in design, you will encounter many pits in implementation. Therefore, fasten your safety belt and get on the bus.

Definition of singleton mode

Singleton mode is to instantiate a globally unique object only once during program operation, which is a bit like static variables in Java, but singleton mode is better than static variables. The JVM will load static variables when the program starts. If they are not used, it will cause a large waste of resources. Singleton mode can realize lazy loading and create instances when using instances. Many tool classes in the development tool class library apply the singleton mode, such as proportional thread pool, cache and log objects. They only need to create one object. If multiple instances are created, it may bring unpredictable problems, such as waste of resources, inconsistent result processing and so on.

Implementation idea of singleton

  • Static instance object
  • Privatize the construction method and prohibit the creation of instances through the construction method
  • Provides a public static method that returns a unique instance

Benefits of singleton

  • There is only one object, with less memory expenditure and good performance
  • Avoid multiple occupation of resources
  • Set global access points in the system to optimize and share resource access

Implementation of singleton mode

There are five ways to write the singleton mode: hungry man mode, lazy man mode, double check lock mode, static internal class singleton mode and enumeration class implementation singleton mode. If you write the lazy mode and double check lock mode improperly, there will be problems such as non singleton or exception of singleton in the case of multithreading. The specific reasons will be explained in the corresponding places below. Let's start with the most basic starving man mode and write our singleton.

Hungry man model

The hungry man pattern takes a simple and crude form. When defining static attributes, it directly instantiates the object. The code is as follows:

//Initialization is completed when the class is loaded, so the class loading is slow, but the speed of obtaining objects is fast
public class SingletonObject1 {
    // Use static variables to store unique instances
    private static final SingletonObject1 instance = new SingletonObject1();

    // Privatized constructor
    private SingletonObject1(){
        // There may be many operations in it
    }

    // Provides an interface for publicly obtaining instances
    public static SingletonObject1 getInstance(){
        return instance;
    }
}

Advantages and disadvantages of hungry man model

advantage

  • The static keyword is used to ensure that all write operations on this variable are completed when referencing this variable, so thread safety at the JVM level is ensured

shortcoming

  • Lazy loading cannot be realized, resulting in a waste of space. If a class is relatively large, we loaded it during initialization, but we haven't used this class for a long time, which leads to a waste of memory space.

Lazy mode

Lazy mode is a lazy mode. Instances will not be created during program initialization, but only when they are used. Therefore, lazy mode solves the problem of space waste caused by hungry mode, and also introduces other problems. Let's take a look at the following lazy mode

public class SingletonObject2 {
    // The instance was not initialized when defining a static variable
    private static SingletonObject2 instance;

    // Privatized constructor
    private SingletonObject2(){

    }

    public static SingletonObject2 getInstance(){
        // When using, first judge whether the instance is empty. If the instance is empty, instantiate the object
        if (instance == null)
            instance = new SingletonObject2();
        return instance;
    }
}

The above is the implementation method of lazy mode, but the above code is not safe in the case of multithreading, because it cannot guarantee that it is single instance mode. There may be multiple instances, which are caused when creating instance objects. Therefore, I propose the instantiated code separately to analyze why there are multiple instances.

1   if (instance == null)
     2       instance = new SingletonObject2();

Suppose that two threads enter the position of 1. Because there are no resource protection measures, the instances that two threads can judge at the same time are empty and will execute the instantiation code of 2. Therefore, there will be multiple instances.

Through the above analysis, we have known the reasons for multiple instances. If we protect resources when creating instances, can we solve the problem of multiple instances? Indeed, we add the synchronized keyword to the getInstance() method to make the getInstance() method a protected resource, which can solve the problem of multiple instances. After adding the synchronized keyword, the code is as follows:

public class SingletonObject3 {
    private static SingletonObject3 instance;

    private SingletonObject3(){

    }

    public synchronized static SingletonObject3 getInstance(){
        /**
         * Adding a class lock affects the performance. After locking, the code is serialized,
         * Most of our code blocks are read operations. In the case of read operations, the code thread is safe
         *
         */

        if (instance == null)
            instance = new SingletonObject3();
        return instance;
    }
}

After modification, the problem of multiple instances is solved, but because the synchronized keyword is introduced and the code is locked, a new problem is introduced. After locking, the program will become serialized. Only the thread that grabs the lock can execute this code block, which will greatly reduce the performance of the system.

Advantages and disadvantages of lazy mode

advantage

  • Lazy loading is realized and memory space is saved

shortcoming

  • Without locking, the thread is unsafe and multiple instances may occur
  • In the case of locking, the program will be serialized, resulting in serious performance problems of the system

Double check lock mode

Let's discuss the problem of locking in lazy mode. For getInstance() method, most operations are read operations. Read operations are thread safe, so we don't have to make each thread hold a lock to call this method. We need to adjust the problem of locking. Thus, a new implementation mode is also generated: double check lock mode. The following is a single example implementation code block of double check lock mode:

public class SingletonObject4 {
    private static SingletonObject4 instance;

    private SingletonObject4(){

    }

    public static SingletonObject4 getInstance(){

        // For the first time, if it is empty, it will not enter the lock grabbing stage and directly return to the instance
        if (instance == null)
            synchronized (SingletonObject4.class){
                // After grabbing the lock, judge whether it is empty again
                if (instance == null){
                    instance = new SingletonObject4();
                }
            }

        return instance;
    }
}

Double check lock mode is a very good singleton implementation mode, which solves the problems of singleton, performance and thread safety. The above double check lock mode looks perfect, but it is actually a problem. In the case of multithreading, null pointer problems may occur. The reason for the problem is that the JVM will optimize and reorder instructions when instantiating objects. What is instruction rearrangement?, Take a look at the following example to briefly understand the sorting of instructions

private SingletonObject4(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();

    }

The above constructor SingletonObject4() is written in the order of 1, 2 and 3. The JVM will reorder its instructions, so the execution order may be 3, 1 and 2, or 2, 3 and 1. Regardless of the execution order, the JVM will ensure that all instances are instantiated in the end. If there are many operations in the constructor, in order to improve efficiency, the JVM will return the object when all the attributes in the constructor are not instantiated. The reason for the null pointer problem of the double detection lock is here. When a thread obtains the lock for instantiation, other threads directly obtain the instance for use. Due to the reordering of JVM instructions, the object obtained by other threads may not be a complete object, so the null pointer exception problem will occur when using the instance.

To solve the problem of null pointer exception caused by double check lock mode, we only need to use volatile keyword, which strictly follows the happens before principle, that is, before the read operation, the write operation must be completed. Singleton mode code after adding volatile keyword:

// Add volatile keyword
    private static volatile SingletonObject5 instance;

    private SingletonObject5(){

    }

    public static SingletonObject5 getInstance(){

        if (instance == null)
            synchronized (SingletonObject5.class){
                if (instance == null){
                    instance = new SingletonObject5();
                }
            }

        return instance;
    }
}

The double check lock mode after adding volatile keyword is a better singleton implementation mode, which can ensure thread safety and no performance problems in the case of multithreading.

Static inner class singleton mode

When the class is created in singleton mode, the properties of the class will not be loaded when the class is created in singleton mode. Because the properties of the class will not be loaded when the class is created in singleton mode. Static attributes are modified by static to ensure that they are instantiated only once, and the instantiation order is strictly guaranteed. The static inner class singleton mode code is as follows:

public class SingletonObject6 {


    private SingletonObject6(){

    }
    // Single case holder
    private static class InstanceHolder{
        private  final static SingletonObject6 instance = new SingletonObject6();

    }

    // 
    public static SingletonObject6 getInstance(){
        // Call internal class properties
        return InstanceHolder.instance;
    }
}

Static inner class singleton mode is an excellent singleton mode, which is commonly used in open source projects. Without any lock, it ensures the safety of multithreading without any performance impact and waste of space.

Enumeration class implements singleton mode

Enumeration class implementation singleton mode is the singleton implementation mode highly recommended by the author of effective java, because the enumeration type is thread safe and can only be loaded once. The designer makes full use of this feature of enumeration to implement the singleton mode. The writing method of enumeration is very simple, and the enumeration type is the only singleton implementation mode that will not be destroyed.

public class SingletonObject7 {


    private SingletonObject7(){

    }

    /**
     * Enumeration types are thread safe and can only be loaded once
     */
    private enum Singleton{
        INSTANCE;

        private final SingletonObject7 instance;

        Singleton(){
            instance = new SingletonObject7();
        }

        private SingletonObject7 getInstance(){
            return instance;
        }
    }

    public static SingletonObject7 getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

Method and solution of destroying single case mode

1. Except the enumeration method, all other methods will destroy a single instance by reflection. Reflection is to generate a new object by calling the construction method. Therefore, if we want to prevent single instance destruction, we can judge in the construction method. If there is an existing instance, we can prevent the generation of a new instance. The solution is as follows:

private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("Instance already exists, please pass getInstance()Method acquisition");
    }
}

2. If the singleton class implements the Serializable serialization interface, it can destroy the singleton through deserialization, so we can not implement the serialization interface. If we have to implement the serialization interface, we can override the deserialization method readResolve(), and directly return the relevant singleton object during deserialization.

public Object readResolve() throws ObjectStreamException {
        return instance;
    }

Original address: https://zhuanlan.zhihu.com/p/80127173 The single cases written in this article are relatively complete. I think I like it very much, so I excerpted it.

Tags: Java Android Design Pattern

Posted by RestlessThoughts on Sat, 14 May 2022 16:23:00 +0300