Interview question: Write a singleton pattern that you think is best

Interview research site

Purpose: The singleton model can cover a lot of ground, so many interviewers will ask this question. Small partners should pay attention to, in the interview process, as long as can examine the ability of candidates from multiple dimensions of the topic, will not be abandoned, especially more general questions, such as: “please tell me your understanding of XXX” and so on.

Scope of investigation: 1 to 5 years of work experience, with the improvement of experience, the deeper the investigation of this problem.

Background knowledge

Singleton pattern is a kind of software design pattern, which belongs to the creation pattern.

It ensures that there is only one instance of a class and provides a global access point.

Based on this feature, we can know that the singleton pattern can avoid the frequent creation of objects for memory consumption, because it limits the creation of instances, in general, it has the following benefits:

  1. Control the use of resources, through thread synchronization to control the concurrent access of resources;

  2. Control the number of instances to save resources.

  3. Used as a communication medium, namely data sharing, it allows multiple unrelated threads or processes to communicate without establishing a direct correlation.

In practice, the singleton pattern is most used in Spring’s IOC container, where Bean management is singleton by default. A bean creates only one object, which is stored in the built-in map, and returns the same object no matter how many times the bean is retrieved.

Let’s look at the design of the singleton pattern.

Singleton pattern design

To ensure that a class has only one instance at runtime, you must not use the new keyword to instantiate it.

So the first step must be to privatize the constructor of the class, which prevents the caller from creating instances of the class themselves.

Then, since the object cannot be instantiated externally, a global access point must be provided to obtain the globally unique instance of the class after internal instantiation. Therefore, we can define a static variable inside the class to reference the unique instance and access the object as the externally provided instance. Based on these points, we can get the following design.

Private static final Singleton INSTANCE = new Singleton(); private static final Singleton INSTANCE = new Singleton(); Private Singleton() {}} private Singleton() {}}Copy the code

Next, we need to give the outside world a method to access INSTANCE of the object. We can provide a static method

Private static final Singleton INSTANCE = new Singleton(); private static final Singleton INSTANCE = new Singleton(); Public static Singleton getInstance() {return INSTANCE; Private Singleton() {}} private Singleton() {}}Copy the code

This completes the design of the singleton pattern. In summary, the singleton pattern is divided into three steps.

  1. Use the private private constructor to ensure that external instantiations cannot be performed;

  2. A private static variable holds a unique instance to ensure global uniqueness.

  3. Return this unique instance through the public static method so that external callers can get the instance.

Other implementations of the singleton pattern

Since the singleton pattern only needs to ensure that only one instance is generated during program execution, that means there are many more implementations of the singleton pattern.

  • Lazy singleton pattern

  • Hunchman singleton pattern

  • DCL double checked singleton

  • Static inner class

  • Enumerated the singleton

  • Implement singletons based on containers

Lazy singleton pattern

Lazy: does not create object instances in advance, but creates them as needed.

public class Singleton { private static Singleton instance; Private synchronized Singleton getInstance() {} public static synchronized getInstance() {if (instance == null) { instance = new Singleton(); } return instance; }}Copy the code

Among them, the synchronized synchronization keyword is added to getInstance() method, in order to avoid the multi-instance problem (thread-safety problem caused by the parallel execution of threads) caused by calling this method at the same time in multi-threaded environment.

Advantages: Singletons are instantiated only when used, saving memory resources to some extent. Disadvantages: instantiated immediately on the first load, slightly slower. Synchronization occurs every time the getInstance() method is called, which can consume unnecessary resources. This mode is generally not recommended.

DCL double checked singleton

DCL double – checked singleton is a performance – optimized version of hunhan-based singleton.

Public class Singleton {private static volatile Singleton instance = null; Private Singleton() {} public static Singleton getInstance() { Synchronized (instance == null) {synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton instance = new Singleton(); } } } return instance; }}Copy the code

As you can see from the code, there are two improvements to the DCL schema:

  1. In the getInstance() method, you narrow down the scope of a synchronized lock.

    Narrowing the scope of the lock provides a performance boost. Consider that loading the synchronized keyword at the method level in the old lazy mode meant that any caller who needed to acquire an instance of the object, whether in a multithreaded or single-threaded environment, needed to acquire the lock. But the lock only protects the instance when it is first initialized. Subsequent accesses should simply return the instance object. So adding Synchroinzed at the method level is bound to incur performance overhead in a multi-threaded environment.

    The transformation of DCL mode is to narrow the scope of locking, only need to protect the instance object in the first initialization, subsequent access, do not need to compete for synchronization lock. So its design is:

  • First, check whether instance instance is empty. If so, add synchronized class level lock to protect instance object’s instantiation process and avoid multi-instance problems in multi-threaded environment.

  • Then within the scope of synchronized synchronization keyword, the instance instance is judged to be empty again, also in order to avoid the multi-instance problem caused by the next thread entering the synchronized code block just after the initialization of the previous thread at the critical point.

  1. The volatile keyword is decorated on the member variable instance to ensure visibility.

    This keyword is added to avoid the visibility problems associated with instruction reordering in the JVM, which are embodied in the instance=new Singleton() code. Let’s look at the bytecode of this code

     17: new           #3                  // class org/example/cl04/Singleton 20: dup 21: invokespecial #4                  // Method "<init>":()V 24: putstatic     #2                  // Field instance:Lorg/example/cl04/Singleton; 27: aload_0 28: monitorexit 29: goto          37 32: astore_1 33: aload_0
    Copy the code

    Focus on the following instructions

    The invokespecial #4 and ASTore_1 instructions allow reordering. The order of execution may be astore_1 first and Invokial #1 later.

    Reordering For two instruction operations that do not depend on each other, CPU and memory, and JVM, execution instructions are reordered to optimize program execution performance. That is, the order in which the two instructions are executed is not necessarily the order in which the program was written.

    After creating an object on the heap to create an address, the address is already fixed, and there is no logical relationship between “associating a Singleton instance on the stack with an object on the heap” and “assigning a member variable in the object”.

    So the CPU can execute out of order, as long as the final result of the program is consistent.

    In this case, there is no problem in a single thread, but in multiple threads, there is an error.

    Imagine that in DCL, thread A just executed the new #4 instruction when the object is new, and then did not execute the invokespecial #4 instruction, but executed astore_1, that is, the instruction reorder occurred.

    Thread B goes to getInstance (), finds that instance is not empty (because it already has a reference to the object, but has not yet assigned a value to a member variable in the object), and returns a “semi-initialized” object (the object has not yet been fully created).

    In the DCL, instance must be volatile because volatile has a memory barrier at the JVM layer that prevents instructions from being reordered and thus ensures program correctness.

  • New #3: This line of instruction says that a space is opened at an address on the heap as a Singleton object

  • Invokespecial #4: This command is used to assign values to member variables in the object

  • Astore_1: This line of instruction relates a Singleton instance on the stack to an object on the heap by reference

On the pros and cons of the DCL model:

Advantages: High resource utilization, it can not only initialize instances when needed, but also ensure thread safety. At the same time, the getInstance() method is not synchronized lock, high efficiency. Cons: slightly slower to load the first time and occasionally fails due to the Java memory model. There are also certain defects in high concurrency environments, although the occurrence rate is very small.

The DCL pattern is the most commonly used singleton pattern implementation, and unless the code is complex in concurrent scenarios, it will generally meet the requirements.

Hunchman singleton pattern

No singleton instances are created at class load time. It is created only when the instance is first requested, and only after the first creation, no further instances of the class are created.

Public class Singleton {private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; }}Copy the code

At runtime, the Java VIRTUAL machine allocates memory for static variables only once, and allocates memory for static variables during class loading.

So the object instance is created at class load time and then retrieved at subsequent access.

The advantages and disadvantages of this model are also obvious.

Advantages: Thread-safe, no need to consider concurrency security.

Disadvantages: Wasted memory space, regardless of whether the object is used or not, will be allocated memory space at startup.

Static inner class

Static inner class is an optimization based on hungrier mode.

Instance is not initialized the first time the Singleton class is loaded, only the first time the getInstance() method is called, the virtual machine loads the SingletonHolder class and initializes instance. The uniqueness of instance and the thread-safety of the creation process are guaranteed by the JVM.

Public Singleton {private Singleton() {} public static Singleton getInstance() {return SingletonHolder.instance; Private static Singleton instance = new Singleton(); private static Singleton instance = new Singleton(); }}Copy the code

This method not only ensures thread-safety, singleton uniqueness, but also delays the initialization of the singleton. It is recommended to use this method to implement the singleton pattern.

Static inner classes are not loaded when the external class is loaded, and static inner classes need not be attached to the external class until they are used, but the external class is also loaded when the static inner class is loaded

An inner class is a static inner class. This inner class belongs to the outer class itself, but not to any object of the outer class. So inner classes that use static modifiers are called static inner classes. Static inner classes have the following rules:

  • A static inner class cannot access instance members of the outer class, only class members of the outer class.

  • An external class can use the class name of a static inner class as a caller to access a class member of a static inner class, or it can use a static inner class object to access an instance member.

Advantages of static inner class singletons:

  • Object creation is thread-safe.

  • Supports delayed loading.

  • There is no need to lock objects.

This is one of the more common patterns.

Implement singletons based on enumeration

Enumerations are the easiest way to implement singletons. This implementation ensures thread-safe and unique instance creation through the nature of Java enumerated types.

public enum SingletonEnum {​    INSTANCE;​    public void execute(){        System.out.println("begin execute");    }​    public static void main(String[] args) {        SingletonEnum.INSTANCE.execute();    }}
Copy the code

Implementing a singleton based on enumerations finds that it does not require the operations described earlier

  1. Privatization of constructors

  2. Instantiated variable references are privatized

  3. The methods for obtaining an instance are common

This way of implementing enumerations is not really safe because private constructs are not protected against reflex attacks.

This approach, advocated by Effective Java author Josh Bloch, not only avoids multithreaded synchronization problems, but also prevents deserialization from recreating new objects, which is a pretty strong barrier.

Implement singletons based on containers

The following code demonstrates a container-based approach to managing singletons.

import java.util.HashMap; import java.util.Map; Public class SingletonManager {private static Map<String, Object> objMap = new HashMap<String, Object>(); public static void regsiterService(String key, Object instance) { if (! objMap.containsKey(key)) { objMap.put(key, instance); } } public static Object getService(String key) { return objMap.get(key); }}Copy the code

SingletonManager can manage multiple singleton types. During program initialization, multiple singleton types are injected into a unified management class, and objects of the corresponding type are obtained according to the key. This approach can be achieved through a unified interface, hiding the implementation and reducing the degree of coupling.

About singleton pattern breaking

One of the problems mentioned earlier in the analysis of the singleton pattern implemented by enumeration classes is that privatized constructs can be broken by reflection, resulting in multi-instance problems.

public class Singleton { private static volatile Singleton instance = null; Private Singleton() {} public static Singleton getInstance() { Synchronized (instance == null) {synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton instance = new Singleton(); } } } return instance; } public static void main(String[] args) throws Exception{ Singleton instance=Singleton.getInstance(); Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton refInstance=constructor.newInstance(); System.out.println(instance); System.out.println(refInstance); System.out.println(instance==refInstance); }}Copy the code

The result is as follows

[email protected]@5cad8086false
Copy the code

Since reflection can break private properties, any singleton implemented through private privatization constructs can be broken by reflection, resulting in multi-instance problems.

One might ask, why should we break singletons when we have nothing to do? There will be no problem with direct access based on this portal, right?

In theory, yes, but suppose you had the following situation?

The following code demonstrates serialization and deserialization of a Singleton through an object stream.

public class Singleton implements Serializable { private static volatile Singleton instance = null; Private Singleton() {} public static Singleton getInstance() { Synchronized (instance == null) {synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton. Class) {if (instance == null) {// Synchronized (Singleton instance = new Singleton(); } } } return instance; } public static void main(String[] args) throws Exception { Singleton instance=Singleton.getInstance(); ByteArrayOutputStream baos=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(baos); oos.writeObject(instance); ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois=new ObjectInputStream(bais); Singleton ri=(Singleton) ois.readObject(); System.out.println(instance); System.out.println(ri); System.out.println(instance==ri); }}Copy the code

The result is as follows

[email protected]@66a29884false
Copy the code

As you can see, serialization also breaks the singleton pattern.

Break tests for enumeration class singletons

One might ask, can’t enumerations be broken?

We can try it. Here’s the code.

public enum SingletonEnum {​    INSTANCE;​    public void execute(){        System.out.println("begin execute");    }​    public static void main(String[] args) throws Exception{        SingletonEnum instance=SingletonEnum.INSTANCE;        Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor();        constructor.setAccessible(true);        SingletonEnum refInstance=constructor.newInstance();        System.out.println(instance);        System.out.println(refInstance);        System.out.println(instance==refInstance);    }}
Copy the code

The result is as follows

Exception in thread "main" java.lang.NoSuchMethodException: org.example.cl04.SingletonEnum.<init>()    at java.lang.Class.getConstructor0(Class.java:3082)    at java.lang.Class.getDeclaredConstructor(Class.java:2178)    at org.example.cl04.SingletonEnum.main(SingletonEnum.java:15)
Copy the code

The error appears to be that there is no empty constructor. There is no proof here that reflection cannot destroy singletons.

The following is the source code for the Enum class. All Enum classes inherit Enum from the abstract class.

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { /** * The name of this enum constant, as declared in the enum declaration. * Most programmers should use the {@link #toString} method rather than * accessing this field. */ private final String name; /** * Returns the name of this enum constant, exactly as declared in its * enum declaration. * * <b>Most programmers should use the {@link #toString} method in * preference to this one, as the toString method may return * a more user-friendly name.</b> This method is designed primarily for * use in specialized situations where correctness depends on getting the * exact name, which will not vary from release to release. * * @return the name of this enum constant */ public final String name() { return name; } /** * The ordinal of this enumeration constant (its position * in the enum declaration, where the initial constant is assigned * an ordinal of zero). * * Most programmers will have no use for this field. It is designed * for use by sophisticated enum-based data structures, such as * {@link java.util.EnumSet} and {@link java.util.EnumMap}. */ private final int ordinal; /** * Returns the ordinal of this enumeration constant (its position * in its enum declaration, where the initial constant is assigned * an ordinal of zero). * * Most programmers will have no use for this method. It is * designed for use by sophisticated enum-based data structures, such * as {@link java.util.EnumSet} and {@link java.util.EnumMap}. * * @return the ordinal of this enumeration constant */ public final int ordinal() { return ordinal; } /** * Sole constructor. Programmers cannot invoke this constructor. * It is for use by code emitted by the compiler in  response to * enum type declarations. * * @param name - The name of this enum constant, which is the identifier * used to declare it. * @param ordinal - The ordinal of this enumeration constant (its position * in the enum declaration, where the initial constant is assigned * an ordinal of zero). */ protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }}Copy the code

This class has a unique constructor that takes two arguments: name and ordinal

Let’s try to create an example using this constructor. The code is shown below.

public enum SingletonEnum {​    INSTANCE;​    public void execute(){        System.out.println("begin execute");    }​    public static void main(String[] args) throws Exception{        SingletonEnum instance=SingletonEnum.INSTANCE;        Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);        constructor.setAccessible(true);        SingletonEnum refInstance=constructor.newInstance("refinstance",2);        System.out.println(instance);        System.out.println(refInstance);        System.out.println(instance==refInstance);    }}
Copy the code

Run the above code and the result is as follows

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)    at org.example.cl04.SingletonEnum.main(SingletonEnum.java:17)
Copy the code

From the error message, we successfully obtained the Constructor, but reported an error in newInstance.

Locate the source code location of the error.

if ((clazz.getModifiers() & Modifier.ENUM) ! = 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatileCopy the code

Get this code from :(clazz.getmodifiers () & Modifier.ENUM)! = 0 When creating objects through newInstance, reflection checks whether the class is ENUM modifier. If so, an exception is thrown and reflection fails. Therefore, enumeration types are absolutely safe for reflection.

Since reflection can’t be destroyed? What about serialization? Let’s try it again

public enum SingletonEnum {​    INSTANCE;​    public void execute(){        System.out.println("begin execute");    }    public static void main(String[] args) throws Exception{        SingletonEnum instance=SingletonEnum.INSTANCE;        ByteArrayOutputStream baos=new ByteArrayOutputStream();        ObjectOutputStream oos=new ObjectOutputStream(baos);        oos.writeObject(instance);        ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());        ObjectInputStream ois=new ObjectInputStream(bais);        SingletonEnum ri=(SingletonEnum) ois.readObject();        System.out.println(instance);        System.out.println(ri);        System.out.println(instance==ri);    }}
Copy the code

The result is as follows.

INSTANCEINSTANCEtrue
Copy the code

Therefore, we can conclude that enumerated types are the only design pattern of all singletons that can avoid reflection breaking leading to multi-instance problems.

In summary, it can be concluded that enumeration is the best practice for implementing the singleton pattern. After all, there are advantages to using it:

  1. Reflective safety

  2. Serialization/deserialization security

  3. Written in simple

Problem solving

Interview question: Write a singleton pattern that you think is best

For this question, we must have the answer, enumeration is the best way to achieve singleton.

Of course, try to explain all aspects of your answer.

  1. The concept of singleton patterns

  2. What are the ways to implement singletons

  3. Strengths and weaknesses of each singleton pattern

  4. The best singleton pattern, and why do you think it’s the best?

The problem summary

The singleton pattern looks simple, but there’s a lot to learn from it.

Examples include thread-safety issues, characteristics of static methods and static member variables, enumerations, reflections, and so on.

How much I want to go back to the past, we only use JSP /servlet, not so much messy knowledge, we just want to be a simple programmer.