Singleton design pattern

Keywords: Java Spring Design Pattern Back-end

1, Application scenario of singleton mode

Singleton Pattern refers to ensuring that a class has absolutely only one instance in any case and providing a global access point. The Singleton Pattern is a creation pattern. Singleton mode is also widely used in real life. For example, national president, company CEO, Department Manager, etc. In J2EE standard, ServletContext, ServletContextConfig, etc; Apply ApplicationContext in the Spring framework; The connection pool of the database is also in the form of singleton.

2, Implementation of singleton mode

1. Hungry Han style

Hungry singleton initializes immediately when the class is loaded and creates a singleton object. Absolute thread safety is instantiated before threads appear, and there can be no access security problem. Advantages: no lock is added, the execution efficiency is relatively high, and the user experience is better than the lazy type. Disadvantages: the class is initialized when it is loaded. If it doesn't work, it takes up space and wastes memory. It may take up a pit and don't shit.

The IOC container ApplicationContext in Spring itself is a typical starving singleton.

A relatively simple way to write:

//Hungry Han style single case
// It initializes immediately when the class is loaded and creates a singleton object

    //Advantages: no lock is added, and the execution efficiency is relatively high,
    //In terms of user experience, it is better than lazy style
	//Absolute thread safety is instantiated before the thread appears. There can be no access security problem

    //Disadvantages: the class is initialized when it is loaded. Whether you use it or not, I occupy space
    //It wastes memory and may occupy the pit without shit

    

public class HungrySingle {
    
     private static final HungrySingle hungrySingle=new HungrySingle();

    private HungrySingle(){
    }

    public static HungrySingle getInstance(){
        return hungrySingle;
    }
}

Another way to write it is to put it in a static code block:

public class StaticHungrySingle {
    
    private static final StaticHungrySingle instance;
    
    static {
        instance = new StaticHungrySingle();
    }
    
    private StaticHungrySingle(){
    }
    
    public static StaticHungrySingle getInstance(){
        return  instance;
    }
}

2. Lazy style

The characteristic of lazy singleton is that the internal class is loaded only when it is called by an external class. Here is a simple example of lazy singleton:

//Lazy single case
//Instantiate only when it needs to be used externally
public class SimpleLazySingle {

    private SimpleLazySingle(){
    }

    //Static block, common memory area
    private static SimpleLazySingle instance=null;

    public static SimpleLazySingle getInstance(){
        if(instance==null){
            instance = new SimpleLazySingle();
        }

        return instance;
    }
}

Although this method is simple, it has serious thread safety problems. In multithreading, problems occur.

Since there is a thread problem, the first solution we think of is locking.

public class SimpleLazySingle {

    private SimpleLazySingle(){
    }

    private static SimpleLazySingle instance=null;

    public synchronized static SimpleLazySingle getInstance(){
        if(instance==null){
            instance = new SimpleLazySingle();
        }

        return instance;
    }
}

Although this writing method can solve the thread safety problem, the concurrency is not high, which affects the performance of the system. If you want to solve the thread safety problem, locking cannot be avoided. If you want to improve the performance, the smaller the locking range, the better. Therefore, the following double check is written:

public class DoubleCheckSingle {

    private volatile static DoubleCheckSingle instance = null;

    private DoubleCheckSingle(){

    }

    public static DoubleCheckSingle getInstance(){
        if(instance==null){
            synchronized (DoubleCheckSingle.class){
                if(instance==null){
                    instance = new DoubleCheckSingle();
                }
            }
        }

        return instance;
    }
}

However, when using the synchronized keyword, it is always locked, which still has a certain impact on the program performance. Is there really no better plan? Of course. We can consider from the perspective of class initialization. Look at the following code and adopt the static internal class method:

public class StaticInnerSingle {

    private StaticInnerSingle(){
    }

    public static final StaticInnerSingle getInstance(){
        return LazyInstance.instance;
    }

    private static class LazyInstance{
        private static final StaticInnerSingle instance=new StaticInnerSingle();
    }
}

This form takes into account both hungry memory waste and synchronized performance. Internal classes must be initialized before method calls, which cleverly avoids thread safety problems.

However, when using the synchronized keyword, it is always locked, which still has a certain impact on the program performance. Is there really no better plan? Of course. We can consider from the perspective of class initialization. Look at the following code and adopt the static internal class method:

//Lazy single case


//This form takes into account both hungry memory waste and synchronized performance
//It perfectly shields these two shortcomings
public class LazyInnerClassSingleton {
    //When LazyInnerClassGeneral is used by default, the internal class will be initialized first
    //If not used, the inner class is not loaded
    private LazyInnerClassSingleton(){
    }

    //Every keyword is not redundant
    //static is to share the space of the singleton
    //Ensure that this method will not be overridden or overloaded
    public static final LazyInnerClassSingleton getInstance(){
        //Before returning the result, the inner class must be loaded first
        return LazyHolder.LAZY;
    }

    //Not loaded by default
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

Why is the writing of static internal classes thread safe and lazy loading?

Virtual opportunity ensures that the class constructor () of a class is correctly locked and synchronized in a multi-threaded environment. If multiple threads initialize a class at the same time, only one thread will execute the class constructor () of this class, and other threads need to block and wait until the active thread executes the () method.

It should be noted that in this case, although other threads will be blocked, if the thread executing the () method exits, other threads will not enter / execute the () method again after waking up, because a type will only be initialized once under the same class loader.

Features of static internal classes: when external classes are loaded, they do not need to load static internal classes. If they are not loaded, they do not occupy memory (delayed loading). The static internal class is loaded only when the external class calls the getInstance method. The static attribute ensures global uniqueness, and the static variable initialization ensures thread safety. Therefore, the synchronized keyword is not added to the method here (the JVM ensures that the initialization of a class is locked synchronously under multithreading)

3. Single case of reflection destruction

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) {
        try{
           
            Class<?> clazz = LazyInnerClassSingleton.class;
            //Construction method of private by reflection
            Constructor c = clazz.getDeclaredConstructor(null);
            //Mandatory access
            c.setAccessible(true);
            //Violent initialization
            Object o1 = c.newInstance();
            //The constructor is called twice, which is equivalent to new twice
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);   //false
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

Obviously, two different instances were created. Now, we make some restrictions in its construction method. Once repeated creation occurs for many times, we will throw an exception directly. Let's look at the optimized construction method. Other codes remain unchanged:

private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("Multiple instances are not allowed");
        }
    }

4. Serialization destruction singleton

When we create a singleton object, sometimes we need to serialize the object and write it to the disk. When we use it next time, we can read the object from the disk and deserialize it into a memory object. The deserialized object will reallocate memory, that is, recreate. If the object of the serialized target is a singleton object, it violates the original intention of the singleton mode, which is equivalent to destroying the singleton. Take a look at the code:

//Deserialization results in singleton corruption
public class SeriableSingleton implements Serializable {

    //Serialization is to convert the state in memory into the form of bytecode
    //So as to convert an IO stream and write it to other places (it can be disk or network IO)
    //The state in memory is permanently saved

    //Deserialization
    //Talk about the persistent bytecode content and convert it to IO stream
    //By reading the IO stream, the read content is converted into Java objects
    //The object new is recreated during the conversion

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}

Write test code:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

From the running results, it can be seen that the deserialized object is inconsistent with the manually created object. It is instantiated twice, which is contrary to the original design intention of the singleton. So, how do we ensure that singletons can be implemented in the case of serialization? In fact, it's very simple. Just add the readResolve() method.

private  Object readResolve(){
        return  INSTANCE;
    }

As for why, I hope readers can explore the source code of the readObject() method of the ObjectInputStream class themselves. I won't explain it here.

In fact, objects are created twice in the process of serialization and deserialization. This happens at the JVM level and is safe. Overriding readResolve just overwrites the deserialized object. Deserialized objects are also recycled by GC.

5. Registered single example

Enumeration [recommended]:

Registered singleton, also known as registered singleton, is to register each instance to a certain place and obtain the instance with a unique ID. There are two ways to write a registered singleton: container cache and enumeration registration. Let's first look at the writing method of enumeration singleton and the code. Create EnumSingleton class:

//Constants are used in constants, which can be shared by everyone.
public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

Test whether serialization and reflection can destroy the single example of the middle writing method:

public class EnumSingletonTest {

    public static void main(String[] args) {
        testSeriable();
    }

    /**
     * Test serialization destruction single example
     */
    public static void testSeriable() {
        try {
            EnumSingleton instance1 = null;

            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());

            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());

        }catch (Exception e){
            e.printStackTrace();
        }
        
    }


    /**
     * Single case of test reflection failure
     */
    public static void testReflect(){
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Tom", 666);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Through the test, it is found that neither serialization nor reflection can destroy the singleton. This kind of writing is more recommended.

Through source code analysis, you can see:

The reason why serialization cannot destroy a singleton: an enumeration type actually finds a unique enumeration object through the Class name and Class object Class. Therefore, the enumeration object cannot be loaded multiple times by the Class loader.

Container cache:
//The method in Spring is to use this registered singleton
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

Posted by picos on Sun, 24 Oct 2021 10:23:28 -0700