Singleton mode implementation

Singleton mode

Singleton Pattern is one of the simplest design patterns in Java. It belongs to creation mode. It is defined as ensuring that a class has only one instance and providing a global access point to access the instance.

The singleton pattern is generally reflected in the class declaration. The singleton class is responsible for creating its own objects and ensuring that only a single object is created. This class provides a way to directly access its unique object without instantiating the object of the class.

Singleton mode has the following advantages:

  • There is only one instance in memory, which reduces the memory overhead, especially the frequent creation and destruction of instances.
  • Avoid multiple occupation of resources (such as writing files).

When choosing to use the singleton mode, we should not only consider its advantages, but also consider that some scenarios must be singleton.

The following describes the implementation of several singleton modes:

1. Hungry Han style

The generation of objects of a class is completed by the class constructor. If a class provides a public constructor, the outside world can create objects of this class at will; Therefore, if you want to limit the generation of objects, one way is to change the constructor to private (at least protected), so that the external class cannot generate objects by reference; at the same time, in order to ensure the availability of classes, you must provide your own instance object and static methods to access this object.

The following is a simple hungry man implementation:

public class Singleton {
    // Private constructor
    private Singleton(){
        
    }
    
    // Instantiate an object inside a class
    // Because you want to access the instance in the static method getInstance(), this field should be decorated with static
    private static Singleton instance = new Singleton();
    
    // Provide a static method to get a unique instance externally
    public static Singleton getInstance() {
        return instance;
    }
}

The following is the test code:

public class SingletonClient {
    public static void main(String[] args) {
        SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance();
        SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance();
        
        System.out.println(simpleSingleton1==simpleSingleton2);
    }
}

// Output results
// true

Advantages of starving Chinese implementation:

  • Hungry man style is a vivid metaphor. For a hungry man, when he wants to use this example, he hopes to get it immediately without any waiting time; Therefore, through the static initialization method, an instance of Singleton is created when the class is loaded for the first time, so as to ensure that it has been initialized when you want to use the instance for the first time.
  • Because the instance is created when the class is loaded, and the class loading process is thread safe, the thread safety problem is also avoided.

Disadvantages of hungry Chinese implementation:

  • When the class is loaded, the object will be instantiated, which may cause unnecessary consumption, because the instance may not be used at all.
  • If this class is loaded multiple times, it will cause multiple instantiations.

The following is a variant of the hungry man mode. In fact, it is the same. The objects are instantiated when the class is loaded:

public class Singleton2 {
    private Singleton2() {
    }
    
    private static Singleton2 instance;
    static {
        instance = new Singleton2();
    }
    
    public static Singleton2 getInstance() {
        return instance;
    }
}

2 lazy style

For the disadvantage 1 of the hungry type, the lazy type can be used, that is, the instance object will not be created and processed in advance, but the instantiation will be delayed until it is referenced for the first time.

public class Singleton {
    // Private constructor
    private Singleton(){
        
    }
    
    // Declare a reference to an instance inside a class
    private static Singleton instance;
    
    // Provide static methods for obtaining instances externally
    public static Singleton getInstance() {
        // Instantiate only when the object is used, and point the reference to the instance object
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

The lazy type has thread safety problems. In the case of multithreading, two threads may enter the if statement at the same time. In this way, when both threads exit from the if, two instance objects will be created, which is no longer a single instance.

To solve the above problems, you can lock the method of creating objects to solve the thread safety problem:

public class Singleton {
    private Singleton(){
        
    }
    
    private static Singleton instance;
    
    // Methods using synchronized modification to realize locking
    public static synchronized Singleton getInstance() {
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

The above method solves the problem of thread safety and can also realize delayed loading, but the efficiency is very low. Synchronization is not required in 99% of cases. The locking range of synchronized is the whole method. All operations of this method are carried out synchronously. However, for the case that the object is not created for the first time, that is, it does not enter the if statement, synchronous operation is not required at all, You can return instance directly.

3 double check lock

The lazy problem of thread safety is that the lock range is too large. The solution is to synchronize the code block and add a smaller lock range:

public class Singleton {
    private Singleton(){
        
    }
    
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null){ // First verification
            synchronized(Singleton.class) { // Lock
                if(instance == null) { // Second verification
                    instance = new Singleton(); // There may be problems here, as explained below
                }
            }
        }
        return instance;
    }
}

The above writing method reduces the lock range by using the synchronization code block, which can greatly improve the efficiency. For the existing instance, there is no need to synchronize and directly return to the instance.

4 double check lock

There seems to be no problem with the implementation of double check lock. It realizes delayed loading, solves the synchronization problem, and reduces the scope of lock to improve efficiency. However, there are still hidden dangers in the code, which is mainly related to the memory model of Java.

The code of double check lock passes instance = new Singleton(); An object is created. This line of code can be decomposed into the following three lines of pseudo code:

memory = allocate(); // Step 1: allocate memory space for the object
ctorInstance(memory); // Step 2: initialize the object
instance = memory; // Step 3: set instance to point to the memory address just allocated

The pseudo code steps 2 and 3 above may be reordered (on some JIT compilers, this reordering actually occurs):

memory = allocate(); // Step 1: allocate memory space for the object
instance = memory; // Step 3: set instance to point to the memory address just allocated
ctorInstance(memory); // Step 2: initialize the object

According to the Java language specification (Java se 7 edition), all threads must comply with intra thread semantics when executing Java programs (intra thread semantics). Intra thread semantics allows the following reordering: in a single thread, the program execution results in a single thread will not be changed. Such reordering can improve the execution performance on the premise of ensuring the execution results.

To better understand the intra thread semantics, please see the following diagram (suppose a thread accesses the object immediately after creating the object):

As long as 2 is guaranteed to be executed before 4, and 1 and 3 are reordered immediately, the execution result is correct, that is, it does not violate the semantics in the thread.

If multiple threads are running, the above reordering will cause problems:

When executing according to the above sequence, thread B will see an object that has not been initialized! However, when thread A accesses for the first time, it sees the object after initialization.

Return to the singleton mode, instance = new Singleton(); Reordering may occur. During the first verification, another thread executing concurrently determines that instance is not null and will directly access the object, but the object has not been initialized at this time.

public class Singleton {
    private Singleton(){
        
    }
    
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null){ // First verification
            synchronized(Singleton.class) { // Lock
                if(instance == null) {  // Second verification
                    instance = new Singleton(); // There may be reordering here
                }
            }
        }
        return instance;
    }
}

After understanding the above problems, there are two schemes to realize thread safe delay initialization:

  • Reordering steps 2 and 3 is not allowed
  • Allow step 2 and step 3 reordering, but do not allow other threads to "see" this reordering

5 solution 1 to the problem of double check lock

For the problem of double check lock in Section 4, solution 1 only needs to make a small modification: declare instance as volatile.

public class Singleton {
    private Singleton(){
        
    }
    
    private static volatile Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null){ // First verification
            synchronized(Singleton.class) { // Lock
                if(instance == null) {  // Second verification
                    instance = new Singleton(); // instance is volatile and will not be reordered
                }
            }
        }
        return instance;
    }
}

This scheme requires JDK5 and above, because the new JSR-133 memory model specification is used from JDK5, which enhances the semantics of volatile.

When the object reference is declared volatile, the reordering of steps 2 and 3 will be prohibited in a multithreaded environment. The sample code will be executed in the following sequence:

This scheme essentially prevents reordering in steps 2 and 3 to ensure thread safe delayed initialization.

6 scheme 2 for solving the problem of double check lock

There is another solution to the problem of double check lock in Section 4: class based initialization. The code is as follows:

public class Singleton {
    private Singleton() {
        
    }
    
    private static class InstanceHolder {
        private static Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

The essence of this scheme is to allow step 2 and step 3 reordering, but not allow other non construction threads to "see" this reordering.

In the implementation code, the first execution of getInstance() method is the active use of InstanceHolder class. Because the static fields of this class are accessed, the active use of a class will trigger the initialization of the class. (in accordance with the following situation 4)

Extension: the following cases also belong to the active use of class or interface type, which will trigger class initialization. It is assumed that the class or interface type is T

1) T is a class, and an instance of type T is created.

2) T is a class, and the static method declared in t is called.

3) The static fields declared in T are assigned.

4) The static field declared in T is used, and this static field is not a constant field.

5) T is a Top Level Class, and an assertion statement is nested inside t and executed.

During class initialization, the JVM will acquire a lock, which can synchronize the initialization of the same class by multiple threads, that is, class initialization is thread safe.

If two threads execute getInstance() method concurrently to initialize the InstanceHolder class, the following figure will be executed:

7 enumeration

At jdk1 After 5, there is another way to implement singleton, using enumeration:

public enum  Singleton {
    INSTANCE;
    Singleton() {
        
    }
}

It can not only avoid the problem of multi-threaded synchronization, but also prevent deserialization from re creating new objects and breaking singletons.

8 singleton and serialization

If a class using Singleton mode needs to be serialized, there is a problem to note: serialization will destroy Singleton. If you want to prevent serialization from destroying singletons, you can solve this problem by defining readResolve() in the Singleton class:

// Serialization destroys singletons
public class Singleton implements Serializable{
    private Singleton(){
        
    }
    
    private static volatile Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null){ 
            synchronized(Singleton.class) { 
                if(instance == null) {  
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
    
    // The following methods are added to solve the problem that serialization will destroy singletons
    private Object readResolve() {
        return instance;
    }
}

9 summary

This paper introduces several implementation methods of singleton mode, and explains the advantages and disadvantages of each method. According to different use scenarios, thread safety, performance and resource occupation need to be considered.

It is recommended to use thread safe double check lock, class initialization and enumeration.

If a class using singleton mode needs serialization, it should be noted that serialization will destroy singleton.

reference material:

  1. http://hollischuang.gitee.io/tobetopjavaer/#/advance/design-patterns/singleton-pattern
  2. Fang Tengfei, Wei Peng, Cheng Xiaoming The art of Java Concurrent Programming [M] China Machine Press, 2015

Keywords: Java Design Pattern Singleton pattern

Added by tomboy78 on Fri, 17 Dec 2021 23:35:55 +0200