Design mode - eight different implementations of single case mode [JAVA] and single case question sorting - interview necessary - super detailed

Design mode - single case mode - eight different implementations [JAVA] and single case problem sorting - necessary for interview - super detailed

Introduction to singleton mode

the singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance.
It can be simply understood as: a class can only create a unique object during the program running time.

8 different implementation versions [JAVA8 implementation]

  1. Common static instance implementation

In essence, it is hungry and Han style. It is loaded at startup, and the implementation is relatively simple, as follows:

/**
 * The common static instance implements the load hungry Han singleton mode when the jvm starts
 * Advantages: the implementation is simple, and the JVM ensures thread safety
 * Disadvantages: initialization and serialization will occur during loading
 *  @author Shiraki
 */
public class DemoMgr1 {
    // Printing times control
    public static final int COUNT = 10;
    // Static instance to ensure that the jvm is initialized when it is started and there is only one copy
    private static final DemoMgr1 instance = new DemoMgr1();

    // private is the object created with new
    private DemoMgr1() {
    }

    // Create instances through static methods
    public static DemoMgr1 getInstance() {
        return instance;
    }

    // test method
    public void m() {
        System.out.println("instance = " + instance.hashCode());
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr1 demoMgr1 = DemoMgr1.getInstance();
                demoMgr1.m();
            }).start();
        }
    }
}

2. Implementation of ordinary static syntax block initialization

Ordinary static instance implementation variants, implemented through static syntax blocks, or hungry Chinese style

/**
 * Ordinary static syntax block initialization implements hungry Chinese singleton mode
 * Advantages: the implementation is simple, and the JVM ensures thread safety
 * Disadvantages: initialization and serialization will occur during loading
 * Static method block implementation
 *
 * @author Shiraki
 */
public class DemoMgr2 {
    public static final int COUNT = 10;

    private static final DemoMgr2 instance ;

    // Initialize static final variables with static statement blocks, the same as DemoMgr2
    static {
        instance = new DemoMgr2();
    }
    private DemoMgr2() {
    }
    public static DemoMgr2 getInstance() {
        return instance;
    }
    public void m() {
        System.out.println("inStance = " + instance.hashCode());
    }


    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr2 demoMgr = DemoMgr2.getInstance();
                demoMgr.m();
            }).start();
        }

3. Ordinary lazy loading singleton implementation - thread unsafe

The starving Chinese problem is that the singleton object will be created whether it is used or not (but if it is not used to write singletons, and the initial loading is delayed, the user will have a delay when using it for the first time; in addition, when the resources are insufficient during the delayed loading, it is better to expose the problem early). Therefore, the method of reloading when it is used is proposed.

/**
 * Common lazy loading singleton implementation lazy type
 * Advantages: delayed loading and simple implementation
 * Disadvantages: thread unsafe
 * @author Shiraki
 */
public class DemoMgr3 {
    public static final int COUNT = 10;
    private static DemoMgr3 instance;
    private DemoMgr3() {
    }

    public static DemoMgr3 getInstance() {
        if (instance == null) {
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

            // Delay initialization, and then initialize when used
            instance = new DemoMgr3();
        }
        return instance;
    }

    public void m() {
        System.out.println("inStance = " + instance.hashCode());
    }
    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr3 demoMgr = DemoMgr3.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

4. Implementation of common lazy loading thread lock version - thread safety

Since you have to reload when you need to use it and ensure the safety of multithreading, you can only add a synchronization mechanism. Take synchronized as an example

/**
 * Common lazy loading thread lock version implementation - thread safe lazy type
 * Advantages: delayed loading, simple implementation, locking to ensure thread safety
 * Disadvantages: heavy lock
 * @author Shiraki
 */
public class DemoMgr4 {
    public static final int COUNT = 10;
    private static DemoMgr4 instance;
    private DemoMgr4() {
    }
    // Lock to ensure thread safety
    public static synchronized DemoMgr4 getInstance() {
        if (instance == null) {
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            // Delay initialization
            instance = new DemoMgr4();
        }
        return instance;
    }
    public void m() {
        System.out.println("inStance = " + instance.hashCode());
    }
    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr4 demoMgr = DemoMgr4.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

5. Dual detection implementation - thread safety

For the implementation of ordinary lazy load thread lock version - thread safety optimization, because it locks the whole getInstance, which will lead to performance degradation. In order to avoid unnecessary locking and conflict, double judgment is used. To filter most scenes that have created instances to obtain them. That is, don't lock the object regardless of the three, seven and twenty-one. First, judge whether the object exists without a lock. If it doesn't exist, lock it again and create the object.

package lxf.design.pattern.singleton;

/**
 * Dual detection lazy
 * Advantages: delayed loading, simple implementation, locking to ensure thread safety, lightweight use of the lock, and pre filter the conditions for entering the lock first
 * Disadvantages: the implementation is troublesome
 * @author Shiraki
 */
public class DemoMgr5 {
    public static final int COUNT = 10;
    // jdk1. Use volatile before 5 to avoid instruction rearrangement
    // Double check needs to pay attention to instruction rearrangement (instruction rearrangement may allocate memory first and then initialize). In case of concurrency, the object has not been initialized when it is not null
    // private volatile static DemoMgr5 instance;
    // jdk1. volatile will not be needed after 5, because the memory model has been optimized to ensure the allocation of memory and the initial atomic operation.
    private static DemoMgr5 instance;
    private DemoMgr5() {
    }
    public static DemoMgr5 getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (DemoMgr5.class) {
                if (instance == null) {
                    // Delay initialization
                    instance = new DemoMgr5();
                }
            }
        }
        return instance;
    }
    public void m() {
        System.out.println("inStance = " + instance.hashCode());
    }
    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr5 demoMgr = DemoMgr5.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

Key points: jdk1 Before 5, volatile was used to avoid instruction rearrangement, jdk1 Volatile will not be needed after 5, because the memory model has been optimized to ensure the allocation of memory and the initial atomic operation.

6. Static internal class implementation - thread safety

The essence of the implementation of double detection is to use locks to judge, which has a certain performance loss. Therefore, the bosses realized the single instance of the combination of hungry and lazy through static private internal classes. I think it's lazy in nature. The implementation is as follows.

/**
 * Static inner class implementation - lazy
 * Advantages: delayed loading, simple implementation, no need to lock
 * @Author: Shiraki
 * @Date: 2020/9/14 14:33
 */
public class DemoMgr6 {
    public static final int COUNT = 10;
    private DemoMgr6() {
    }
    // Static internal class, which can access private construction methods
    private static class DemoMgr6Hander {
        private final static DemoMgr6 instance = new DemoMgr6();
    }
    // Public static methods can obtain static private internal classes. So as to realize the lazy loading of singletons through static internal classes
    public static DemoMgr6 getInstance() {
        return DemoMgr6Hander.instance;
    }
    public void m() {
        System.out.println("inStance = " + DemoMgr6Hander.instance.hashCode());
    }

    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr6 demoMgr = DemoMgr6.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

7. Enumeration singleton - thread safety, serialization safety

In fact, there is no way to avoid the problems of reflection and serialization in the implementation of the above single example. Then, boss Joshua Bloch proposed a single element enum type is often the best way to implement a singleton. Enumeration singleton is to solve the problem of destroying singleton by reflection and serialization.

/**
 * Enumeration implementation singleton mode -- Enumeration deferred loading -- lazy 
 * Advantages: delayed loading, simple implementation, no locking, preventing reflection and serialization
 * @Author: Shiraki
 * @Date: 2020/9/14 14:33
 */
public class DemoMgr6 {
    public static final int COUNT = 10;
    private DemoMgr6() {
    }
    // Static internal class, which can access private construction methods
    private static class DemoMgr6Hander {
        private final static DemoMgr6 instance = new DemoMgr6();
    }
    // Public static methods can obtain static private internal classes. So as to realize the lazy loading of singletons through static internal classes
    public static DemoMgr6 getInstance() {
        return DemoMgr6Hander.instance;
    }
    public void m() {
        System.out.println("inStance = " + DemoMgr6Hander.instance.hashCode());
    }

    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr6 demoMgr = DemoMgr6.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

8. Double detection - thread safety, serialization safety, reflection safety (just understand)

Toutie doesn't want to use enumeration to realize serialization and reflection security. What should I do??? as follows

/**
 * Double detection added River version lazy type
 * Advantages: delayed loading, simple implementation, locking to ensure thread safety, lightweight use of the lock, pre filter the conditions for entering the lock first,
 * Support to prevent reflection and serialization attacks
 * @author Shiraki
 */
public class DemoMgr5Ex implements Serializable {
    public static final int COUNT = 10;
    // jdk1. Use volatile before 5 to avoid instruction rearrangement
    // Double check needs to pay attention to instruction rearrangement (instruction rearrangement may allocate memory first and then initialize). In case of concurrency, the object has not been initialized when it is not null
    // private volatile static DemoMgr5 instance;
    // jdk1. volatile will not be needed after 5, because the memory model has been optimized to ensure the allocation of memory and the initial atomic operation.
    private static DemoMgr5Ex instance;
    private DemoMgr5Ex() {
        // Judge whether the object has been created by throwing an exception. If so, throw an exception. It should actually be locked here
        synchronized (DemoMgr5Ex.class) {
            if (instance != null) {
                throw new RuntimeException();
            }
        }
    }
    // Avoid deserialization.
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
    public static DemoMgr5Ex getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (DemoMgr5Ex.class) {
                if (instance == null) {
                    // Delay initialization
                    instance = new DemoMgr5Ex();
                }
            }
        }
        return instance;
    }
    public void m() {
        System.out.println("inStance = " + instance.hashCode());
    }
    public static void main(String[] args) {
        for (int i = 0; i < COUNT; i++) {
            new Thread(()->{
                DemoMgr5Ex demoMgr = DemoMgr5Ex.getInstance();
                demoMgr.m();
            }).start();
        }
    }
}

Q&A

- Q1. Since the above singleton implementations except enumeration will have the problems of reflection and serialization, is there a solution?
A1. For reflection, you can add a static variable flag to mark whether the object is created, and then judge the flag in the private construction method, or throw an exception directly in the private construction method to prevent the destruction of a single instance through reflection; For serialization, you can avoid the destruction of deserialization by defining the Object readResolve() method.

/**
 * Reflection will destroy the singleton, and reflection attack singleton mode test
 * @Author: Shiraki
 * @Date: 2020/9/14 16:56
 */
public class SingletonAndReflect {
    public static void main1() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println("main1===============================");
        DemoMgr5 singleton = DemoMgr5.getInstance();
        DemoMgr5 singleton2 = DemoMgr5.getInstance();
        System.out.printf(" singleton ?= singleton2 [%b]\n", singleton == singleton2);
        Class cls = Class.forName("lxf.design.pattern.singleton.DemoMgr5");
        Constructor constructor = cls.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DemoMgr5 singleton3 = (DemoMgr5) constructor.newInstance(null);
        System.out.printf(" singleton ?= singleton3 [%b]\n", singleton == singleton3);
    }
    public static void main2() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println("main1===============================");
        DemoMgr5Ex singleton = DemoMgr5Ex.getInstance();
        DemoMgr5Ex singleton2 = DemoMgr5Ex.getInstance();
        System.out.printf(" singleton ?= singleton2 [%b]\n", singleton == singleton2);
        Class cls = Class.forName("lxf.design.pattern.singleton.DemoMgr5Ex");
        Constructor constructor = cls.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        // Creating an object throws a runtime exception
        DemoMgr5Ex singleton3 = (DemoMgr5Ex) constructor.newInstance(null);
        System.out.printf(" singleton ?= singleton3 [%b]\n", singleton == singleton3);
    }


    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        main1();
        main2();
        
    }

}
/**
 * output:
 * main1===============================
 *  singleton ?= singleton2 [true]
 *  singleton ?= singleton3 [false]
 * main1===============================
 *  singleton ?= singleton2 [true]
 * Exception in thread "main" java.lang.reflect.InvocationTargetException
 * 	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 * 	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 * 	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 * 	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 * 	at lxf.design.pattern.singleton.SingletonAndReflect.main2(SingletonAndReflect.java:31)
 * 	at lxf.design.pattern.singleton.SingletonAndReflect.main(SingletonAndReflect.java:38)
 * Caused by: java.lang.RuntimeException
 * 	at lxf.design.pattern.singleton.DemoMgr5Ex.<init>(DemoMgr5Ex.java:18)
 * 	... 6 more
 */

- Q2. How to understand the classification of single cases?
A2. Personally, I think the dimensions of lazy and hungry classification are distinguished by the different creation times of instance classes. If the classification is implemented in the above 8, there are only lazy and hungry. Double detection, enumeration singleton and static method singleton are lazy loading in essence, which can be attributed to lazy loading. It's just a different way of implementation.

- Q3. How can a singleton be applied to a cluster environment?
A2. Can only rely on distributed locks.

- Q4. How to implement multiple instances?
A2. Use the container (Map) to specify the container capacity.

- Q5. How to implement thread singleton?
A2. Use the ThreadLocal utility class.

reference resources

Avoid reflection and serialization to break singletons
Beauty of design pattern

Tags: Java jvm Design Pattern Interview Multithreading

Posted by SalokinX on Mon, 16 May 2022 21:13:29 +0300