Learning Notes: A Single Case Pattern for Java Design Mode

Singleton mode

Definition: Ensure that a class has only one instance and provide a global access point

Type: Creative

Use scenarios:

To ensure there is absolutely one instance in any case

Advantage:

Only one instance in memory reduces memory overhead

Avoid multiple occupation of resources

Set up global access points and strictly control access

Disadvantages:

No interface, difficult to extend

A key:

Private Constructor

Thread Security

Delayed Loading

Serialization and deserialization security

Prevent reflection

Lazy:

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton(){}

    public static LazySingleton getInstance() {

        if (lazySingleton == null) {

            lazySingleton = new LazySingleton();

        }

        return lazySingleton;

    }

}

Multithreaded debug:

public class T implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + lazySingleton);
    }
}

public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }

}

Result: In the case of multi-threaded debug manual intervention breakpoint, the object acquired in thread 0 and thread 1 singleton mode is not the same, and there are hidden dangers in the program

Connected to the target VM, address: '127.0.0.1:52373', transport: 'socket'
Thread-0 main.java.creational.singleton.LazySingleton@31309637
Thread-1 main.java.creational.singleton.LazySingleton@42fc6f97
program end

After improvement:

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton(){}

    // Evolution
    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

Because of the direct locking in the static method, the object being locked is the current class, which has some overhead. Let's look at another way - double check:

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    // 1. Allocate memory space
                    // 3. Point to memory space
                    // 2. Initialize Objects
                    // Reordering of instructions
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

Since instruction reordering is involved in the code, there are two ways to resolve it, one is to prohibit reordering, the other is to allow reordering but not allow other threads to see it.

The first solution prohibits reordering, using the volatile keyword:

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    // 1. Allocate memory space
                    // 3. Point to memory space
                    // 2. Initialize Objects
                    // Reordering of instructions
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

The second solution, static internal classes:

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){}

    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }

}

Singleton pattern - Hungry Han style:

public class HungrySingleton {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

}

Serialization and deserialization issues:

Test class:

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

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

    }

}

Test class runtime error:

Connected to the target VM, address: '127.0.0.1:53906', transport: 'socket'
Exception in thread "main" java.io.NotSerializableException: main.java.creational.singleton.HungrySingleton
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at main.java.creational.singleton.Test.main(Test.java:16)
Disconnected from the target VM, address: '127.0.0.1:53906', transport: 'socket'

Process finished with exit code 1

HungrySingleton is required to implement the serialization interface

Run again as follows: instance and newInstance are not the same object, and the singleton is destroyed after serialization and deserialization

main.java.creational.singleton.HungrySingleton@7a81197d
main.java.creational.singleton.HungrySingleton@4769b07b
false

Process finished with exit code 0

Solution: Add readResolve() to HungrySingleton. This article only provides a solution. See the source code for details

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    private Object readResolve() {
        return hungrySingleton;
    }

}

Reflection Attack:

Test class:

public class Test {

    public static void main(String[] args)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        // Release privileges on private constructors
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

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

}

Run result:

main.java.creational.singleton.HungrySingleton@29453f44
main.java.creational.singleton.HungrySingleton@5cad8086
false

Process finished with exit code 0

Reflection Attack Solution: Throw Runtime Exceptions in Private Constructors (also applicable to StaticInnerClassSingleton, static internal class, not LazySingleton)

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("Singleton mode prohibits reflection calls");
        }
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    private Object readResolve() {
        return hungrySingleton;
    }

}

Keywords: Java Algorithm Back-end Singleton pattern

Added by Benny007 on Tue, 14 Dec 2021 19:08:38 +0200