[Java] Detailed explanation of singleton design pattern

Pattern definition : ensuring that a class has only one instance and providing a global access point, which is a creational pattern

Usage scenarios : heavyweight objects that do not require multiple instances, such as thread pools and database connection pools

Implementation of singleton design pattern

1. Lazy mode: lazy loading, instantiation only when actually used

public class LazySingletonTest {
    
    
    public static void main(String[] args) {
    
    
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton instance1 = LazySingleton.getInstance();
        System.out.println(instance == instance1);
    }
}
class LazySingleton{
    
    

    private static LazySingleton instance;

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

This is the most basic concept of the lazy mode, which is to create it when it is needed.

However, this design may have thread safety issues. Two new instances may be created during two accesses at the same time. The
simplest solution is to lock getInstance.

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

The new problem is that the method locks every time, which is completely unnecessary and is a waste of performance.
Then you can delay a lock.

    public synchronized static LazySingleton getInstance(){
    
    
        if (instance == null){
    
    
        	synchronized (LazySingleton.class){
    
    
        		if(instance == null){
    
    	//还是防止第一次初始化两个线程竞争
        			instance = new LazySingleton();
        		}
        	}
        }
        return instance;
    }

From the bytecode perspective, the JIT or CPU will reorder the initialization and reference assignment of instance = new LazySingleton(); in this line of code. Another
thread accesses the reference during the copying without initialization, and finds that the instance has been assigned. , he will directly get the static instance instead of entering the if, which will cause him to get it, but the instance he gets is empty because it is not initialized. You can use volatile to prevent instruction reordering to solve this problem
.

	private volatile static LazySingleton instance;

2. Hungry Man Mode:

The initialization phase of class loading completes the initialization of the class. Essentially, it relies on the loading mechanism of the jvm class to ensure the uniqueness of the instance.

public class hungrySingletonTest {
    
    
    public static void main(String[] args) {
    
    
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton instance1 = HungrySingleton.getInstance();
        System.out.println(instance1 == instance);
    }
}

class HungrySingleton{
    
    
    private static HungrySingleton instance = new HungrySingleton();
    //不允许外部进行实例化
    private HungrySingleton(){
    
    }
    public static HungrySingleton getInstance(){
    
    
        return instance;
    }
}

Hungry mode assignment is completed during the initialization phase of class loading , and class loading triggers initialization only when the corresponding class is actually used.

3. Static inner class pattern:

The essence is also a lazy loading method implemented through the class loading mechanism.

class InnerClassSingleton{
    
    
    private static class InnerClassHolder{
    
    
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    
    }
    public static InnerClassSingleton getInstance(){
    
    
        return InnerClassHolder.instance;
    }
}

It is slightly different from the hungry mode. The hungry mode directly initializes the instance when the class is loaded. In the static inner class method, if the getInstance() method is not called, the static inner class InnerClassHolder will not be called. When loading, the instance will not be initialized, so it is also a kind of lazy loading.
In essence, it also uses the loading mechanism of the class to ensure the thread safety of the class.

Prevent reflection attacks

This singleton design pattern makes it possible to obtain new instance objects through reflection, such as

public class InnerClassSingletonTest {
    
    
    public static void main(String[] args) throws Exception {
    
    
    	//通过反射获取他的构造器,然后通过构造器getInstance一个对象
        Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();

		//正常方法获取对象
        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        //返回flase
		System.out.println(instance == innerClassSingleton);
    }
}

class InnerClassSingleton{
    
    
    private static class InnerClassHolder{
    
    
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    
    }
    public static InnerClassSingleton getInstance(){
    
    
        return InnerClassHolder.instance;
    }
}

Preventive measures are given below. For example, in Hungry mode and static inner class mode, you can determine whether it has been initialized in the constructor. If so, it means that the instance is obtained through abnormal means such as reflection, and then throws abnormal

	class InnerClassSingleton{
    
    
    private static class InnerClassHolder{
    
    
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    
    
        if (InnerClassHolder.instance != null) {
    
    
            throw new RuntimeException("单例不允许多个实例!");
        }
    }
    public static InnerClassSingleton getInstance(){
    
    
        return InnerClassHolder.instance;
    }
}

Obviously, lazy mode has no way to prevent this reflection

enumeration type

First, let’s take a look at the source code of the constructor’s newInstance() method in reflection.

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
    
    
        if (!override) {
    
    
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
    
    
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //-------!!!!!!!!!!!!!!!!!!!!---------
        //由这行代码可知,如果使用反射访问的类是个枚举类,那么就会抛出异常
        //由这个思路可以用枚举来防止反射攻击
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
    
    
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

The following is the most basic singleton pattern designed using enumerations

public class EnumSingletonTest {
    
    
    public static void main(String[] args) {
    
    
        EnumSingleton instance = EnumSingleton.INSTANCE;
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        System.out.println(instance1 == instance);
    }
}
enum EnumSingleton{
    
    
    INSTANCE;
    public void print(){
    
    
        System.out.println(this.hashCode());
    }
}

By looking at the bytecode file, we can see that
Constructor of EnumSingleton under bytecode file
the enum class actually inherits java.lang.Enum, and
we can see the constructor of Enum.
Constructor of Enum class
So if we want to access it, we must also get the constructor in reflection
and finally use the constructor. When newInstance comes out
The result diagram of newInstance after reflecting the enumeration class
, you can see that it is very good at preventing reflection attacks.

Serialized singleton

In actual work, we need the class to be serializable (such as implements Serializable), and then transfer data to
an Instance. After writing and reading operations, the reading will not go through the constructor, so a new instance will be generated, destroying Singleton form

//在静态内部类那个类做了个测试
public class InnerClassSingletonTest {
    
    
    public static void main(String[] args) throws Exception {
    
    

        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
        oos.writeObject(instance);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
        InnerClassSingleton instance1 = (InnerClassSingleton)ois.readObject();
		ois.close();
        System.out.println(instance1 == instance); 	//输出false
    }
}

class InnerClassSingleton implements Serializable {
    
    
    private static class InnerClassHolder{
    
    
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    
    
        if (InnerClassHolder.instance != null) {
    
    
            throw new RuntimeException("单例不允许多个实例!");
        }
    }
    public static InnerClassSingleton getInstance(){
    
    
        return InnerClassHolder.instance;
    }
}

Let’s take a look at the official solution.
Insert image description here
It means implementing a special method and then returning the singleton you want.

public class InnerClassSingletonTest {
    
    
    public static void main(String[] args) throws Exception {
    
    

        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
        oos.writeObject(instance);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
        InnerClassSingleton instance1 = (InnerClassSingleton)ois.readObject();

        System.out.println(instance1 == instance);
    }
}

class InnerClassSingleton implements Serializable {
    
    
    static final long serialVersionUID = 42L;	//序列化版本号
    private static class InnerClassHolder{
    
    
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
    
    
        if (InnerClassHolder.instance != null) {
    
    
            throw new RuntimeException("单例不允许多个实例!");
        }
    }
    public static InnerClassSingleton getInstance(){
    
    
        return InnerClassHolder.instance;
    }
	//实现的readResolve方法
    Object readResolve() throws ObjectStreamException{
    
    
        return InnerClassHolder.instance;
    }
}

PS: Remember to write down a version number, otherwise an error will be reported

Guess you like

Origin blog.csdn.net/qq_67548292/article/details/131950191