There are so many ways to write singleton mode (analysis of JAVA singleton mode)

Posted by Cong on Thu, 16 Dec 2021 06:29:21 +0100

Lazy single case

First, write a simple example of the lazy pattern

public class SimpleSingleton {
    private static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (singleton == null) { // 1. Judge whether it is empty
            singleton = new SimpleSingleton(); //2. Perform initialization
        }
        return singleton;
    }
}

Lazy means I'm so lazy that I'll prepare when I want to eat

For a singleton, you should try to create a singleton object every time you use it.

The above singleton mode has some problems. The first thing you will think of is multithreading.

Because 1 and 2 are not atomic operations

If in the case of multi-threaded access, one of the threads executes step 2

If the initialization of the member variable is not completed, another thread will proceed to step 1 at this time to judge that the member variable is still empty and will execute the initialization of instance. In this way, multiple threads will perform initialization operations to obtain different singleton objects, which does not meet the requirements of singleton.

Now that you understand the principle, you can carry out the next test and borrow the Debug tool of IDEA

Block the first thread Thread0 and continue to execute other threads when instance is initialized

The final result is that the objects obtained for Thread0 and other threads are not the same

If there is a multithreading problem, the first method we think of is locking, as shown below

public static SimpleSingleton getInstance() {
        synchronized (SimpleSingleton.class) {
            if (singleton == null) { // 1. Judge whether it is empty
                singleton = new SimpleSingleton(); //2. Perform initialization
            }
        }
        return singleton;
    }

In this way, step 1 and step 2 become atomic operations, but also lead to the serialization of multithreading, which has a certain impact on the efficiency,

On this basis, another solution has emerged. Step 1 is advanced through Double Checking Locking(DCL), which is equivalent to that when other threads obtain instances after instance initialization, there will be no locking and unlocking overhead. After all, locks still affect performance.

After modification, the code is as follows

public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1. Judge whether it is empty
                    singleton = new SimpleSingleton(); //2. Perform initialization
                }
            }
        }
        return singleton;
    }

However, there will still be problems with the above code, so we need to go back to the operation performed at the bottom in step 2, which is divided into three operations:

(1) Allocate a block of memory

(2) Initializing member variables in memory

(3) Point the instance reference to memory

Because there will be reordering before operations (2) and (3), that is, execute instance to memory first and initialize member variables.

At this point, another thread may get an object that is not fully initialized.

If you directly access the member variables in the singleton object, an error may occur.

This causes a typical "constructor overflow" problem.

The solution is also relatively simple. Use volatile to modify instance. The code is as follows:

private volatile static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1. Judge whether it is empty
                    singleton = new SimpleSingleton(); //2. Perform initialization
                }
            }
        }
        return singleton;
    }

In this way, the problem of multithreading safety is solved. As for the role of volatile keyword, there is much more to talk about. For the time being, we will only talk about single case related/

Of course, the lazy man's singleton mode is not used very much in actual projects. Let's analyze the hungry man's singleton mode.

Single case of hungry man

The meaning of a hungry man is that I've always been hungry, so I have to prepare all the food. I'll eat if I want;

public class HungrySingleton {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

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

It seems that the hungry man is relatively simple. For the example, you can refer to the Runtime class in Java, which is a very typical hungry man mode

There are problems, serialization and deserialization problems

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singletonFile"));
        out.writeObject(instance);

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singletonFile"));
        HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
    }
}

Execution result: instance and newInstance are not the same object

This requires a careful reading of the deserialization source code. Through the code reading, you will find that the deserialization creates an object through the reflection method, resulting in that the object after deserialization is not the same as that before serialization

For detailed code reading, please refer to the link Singleton, serialization, and readResolve() methods - Nuggets

Solution code

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }   

    // Used to solve serialization and deserialization problems
    public Object readResolve() {
        return instance;
    }
}

During deserialization, the readResolve method will be called to obtain the original object through reflection and replace it, so as to ensure that the objects before and after serialization are the same.

Since this problem occurs during deserialization, it is also possible that someone in the project will use reflection to create the object, which will cause another problem,

The simple solution is to prohibit the instance from being created through reflection

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("Prohibit creation by reflection");
        }
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public Object readResolve() {
        return instance;
    }
}

Of course, this method cannot be used for lazy mode, because lazy mode belongs to delayed creation. Throw errors when it cannot be created.

Static inline singleton

The hungry Han singleton also has the problem of delayed loading. For example, instance will be initialized with the project. It may not be used for a long time in the later project operation, but it is always resident in memory and wastes resources.

It can be solved through the method of Inner Class

public class StaticInnerClassSingleton {

    public static class InnerClass {
        static {
            System.out.println("inner class");
        }
        private static StaticInnerClassSingleton staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        System.out.println("get instance");
        return InnerClass.staticInnerClassSingleton;
    }

    public static void main(String[] args) {
        StaticInnerClassSingleton.getInstance();
    }
}

This ensures that the object is initialized in memory when it is needed.

Container single example

public class ContainerSingleton {

    private static HashMap map =  new HashMap<String, Object>();
    private ContainerSingleton() {}
    public static void putInstance(String key, Object o) {
        if (key != null && key.length() > 0 && o != null){
            map.put(key,o);
        }
    }

    public static Object getInstance(String key) {
        return map.get(key);
    }

}

Let's go directly to the code, which is also common in the project. We borrow the key value structure of map to ensure the uniqueness of singleton.

For specific examples, refer to the SingletonBeanRegistry interface in Spring

Enumeration singleton

The simplest and safest way to write

public enum EnumSingleton {
    INSTANCE {
    };

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

ThreadLocal singleton

Thread dependent singleton mode

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
        @Override
        protected Object initialValue () {

            return new ThreadLocalSingleton();
        }
    };

    private ThreadLocalSingleton()
    {}

    public static ThreadLocalSingleton getInstance(){
        return threadLocal.get();
    }
}

The {ErrorContext in Mybatis uses this singleton pattern

But pay attention to garbage collection

Original link: There are so many ways to write singleton mode (analysis of JAVA singleton mode)

Topics: Java Singleton pattern