This is the ninth day of my participation in the First Challenge 2022. For details: First Challenge 2022.

Introduction to the

The Singleton Pattern is one of the simplest design patterns in Java. This type of design pattern is the creation pattern, which provides the best way to create objects.

This pattern involves a single class that is responsible for creating its own objects while ensuring that only a single object is created. This class provides a way to access its unique objects directly, without instantiating the objects of the class.

The singleton pattern primarily addresses the frequent creation and consumption of a globally used class to improve overall code performance.

The characteristics of

  1. A singleton class can have only one instance.

  2. A singleton class must create its own unique instance.

  3. The singleton class must provide this instance to all other objects.

The advantages and disadvantages

  1. Advantages: There is only one instance in memory, which reduces memory overhead, especially the frequent creation and destruction of instances.

  2. Disadvantages: No interface, no inheritance, conflicts with the single responsibility principle, a class should only care about internal logic, not how to instantiate outside.

Implementation approach

Create a class that privates its default constructor so that new Object cannot be used to obtain an instance of an Object and provides a method for obtaining a unique instance of an Object.

For example, create a SingleObject like this:

public class SingleObject {
 
   // Create an object for SingleObject
   private static SingleObject instance = new SingleObject();
 
   // Make the constructor private so that the class is not instantiated
   private SingleObject(a){}
 
   // Get the only available object
   public static SingleObject getInstance(a){
      return instance;
   }
 
   public void showMessage(a){
      System.out.println("Hello World!"); }}Copy the code

Get a unique object from the Singleton class

public static void main(String[] args) {
    // Invalid constructor
    // Compile-time error: constructor SingleObject() is not visible
    //SingleObject object = new SingleObject();

    // Get the only available object
    SingleObject object = SingleObject.getInstance();

    // Displays the message
    object.showMessage();
}
Copy the code

Execute the program and output the result:

Hello World! 
Copy the code

implementation

Lazy threads are not safe

public class Singleton {  

    private static Singleton instance;  
    
    private Singleton (a){}  
  
    public static Singleton getInstance(a) {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	returninstance; }}Copy the code

This is the most basic implementation, and the biggest problem with this implementation is that it does not support multithreading. Synchronized is not strictly a singleton mode because it is not locked. This approach to lazy loading is obviously not thread-safe and does not work well in multiple threads.

Lazy, thread-safe

public class Singleton {  

    private static Singleton instance;  
    
    private Singleton (a){}  
    
    public static synchronized Singleton getInstance(a) {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	returninstance; }}Copy the code

This approach is very lazy and works well in multiple threads, but it is inefficient and does not require synchronization 99% of the time.

Advantages: Initialization is performed only on the first call, avoiding memory waste.

Disadvantages: synchronized must be added to ensure singletons, but locking will affect efficiency.

The performance of getInstance() is not critical to the application (this method is used infrequently).

The hungry type

public class Singleton {  

    private static Singleton instance = new Singleton();  
    
    private Singleton (a){}  
    
    public static Singleton getInstance(a) {  
    	returninstance; }}Copy the code

This approach is common, but tends to generate garbage objects.

Advantages: Without locking, the execution efficiency will be improved.

Disadvantages: Class initialization on load, waste of memory.

It avoids multithreading synchronization problems based on the ClassLoader mechanism. However, instance is instantiated when the class is loaded. Although there are many reasons for class loading, in singleton mode the getInstance method is mostly called, However, there is no other way (or static way) to load the class, so initializing instance is obviously not lazy.

Double check lock type

The Singleton only needs to be instantiated once before it can be used directly. The lock is only required for the instantiated part of the code, and is only required if the Singleton is not instantiated.

The double check lock checks whether the singleton is instantiated and locks the instantiated statement if it is not.

public class Singleton {  

    private volatile static Singleton singleton;  
    
    private Singleton (a){}  
    
    public static Singleton getSingleton(a) {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = newSingleton(); }}}returnsingleton; }}Copy the code

Consider the following implementation, which uses only one if statement. In the case of singleton == null, if both threads execute an IF statement, both threads will enter the if statement block. Singleton = new Singleton(); singleton = new Singleton(); This statement, just in order, will be instantiated twice. Therefore, a double check lock must be used, that is, two if statements are required: The first if statement is used to avoid locking after the singleton has already been instantiated, while the second if statement is used to lock, so that only one thread can enter, so that two threads do not instantiate at the same time when singleton == null.

if (singleton == null) {
    synchronized (Singleton.class) {
        singleton = newSingleton(); }}Copy the code

Singleton = new singleton (); This code is actually executed in three steps:

  1. Allocate memory space for singleton
  2. Initialize the singleton
  3. Point the Singleton to the allocated memory address

However, due to the reordering nature of the JVM, the order of execution can be 1>3>2. Instruction reordering is not a problem in a single-threaded environment, but can cause a thread to acquire an instance that has not yet been initialized in a multi-threaded environment. For example, thread T1 executes 1 and 3, and T2 calls getSingleton() to find that the Singleton is not empty, so it returns the Singleton, but the singleton has not yet been initialized.

Using volatile prevents the JVM from reordering instructions, ensuring that they can run properly in multithreaded environments.

Static inner class

public class Singleton {  
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (a){}  
    
    public static final Singleton getInstance(a) {  
    	returnSingletonHolder.INSTANCE; }}Copy the code

When the Singleton class is loaded, the static inner class SingletonHolder is not loaded into memory. Singletonholder.instance is loaded only when the getInstance() method is invoked, and the INSTANCE INSTANCE is initialized, And the JVM can ensure that INSTANCE is instantiated only once.

This approach not only has the benefit of delayed initialization, but also provides thread-safe support by the JVM.

Enumeration type

public enum Singleton {

    INSTANCE;

    private String objName;


    public String getObjName(a) {
        return objName;
    }

    public void setObjName(String objName) {
        this.objName = objName;
    }

    public static void main(String[] args) {
        // Singleton test
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());
        // Reflection get instance test
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for(Singleton enumConstant : enumConstants) { System.out.println(enumConstant.getObjName()); }}catch(Exception e) { e.printStackTrace(); }}}Copy the code
firstName
secondName
secondName
secondName
Copy the code

This implementation prevents reflection attacks. In other implementations, the setAccessible() method sets the access level of the private constructor to public and then calls the constructor to instantiate the object. To prevent this attack, you need to add code to the constructor that prevents multiple instantiations. This implementation is guaranteed by the JVM to be instantiated only once, so the above reflection attacks do not occur.

This implementation does not get multiple instances after multiple serialization and serialization. Other implementations need to use transient to decorate all fields and implement serialization and deserialization methods.

conclusion

In general, the first and second lazy ways are not recommended, and the third hungry way is recommended. The fifth static inner class method is used only when the lazy loading effect is explicitly implemented. If it comes to deserializing and creating objects, you can try the sixth method of enumeration. If you have other special requirements, you can consider using a fourth double-check mode.