The six types of singleton mode, how to ensure thread safety, reflection safety and serialization safety, are finally clear this time

Keywords: Java Design Pattern Singleton pattern

1, Foreword

The singleton mode belongs to the creation mode, which ensures that there is only one instance of the singleton class in the system. It can avoid frequent creation of an object and reduce memory consumption to a certain extent.

This article will explain various implementation types of singleton classes, and explain the measures to ensure thread safety, reflection safety and serialization safety from the source level.

2, Implementation types of singleton mode

Hungry Han style

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

}

advantage:

Thanks to the class loading mechanism (for the class loading mechanism, please refer to another article of mine There is so much to say behind a new object ), it will lock and execute all static methods during initialization, which directly avoids the problem of multi-threaded synchronization in use

Disadvantages:

Whenever an instance of the current class is used, an instance object will be created before it is officially used.

If this pattern is used in all external jar s we rely on, a large number of instances will be stored in memory in advance. We may not use the instance object from beginning to end, resulting in a waste of memory to a certain extent.

Lazy style

Solve the problem of starving and wasting memory

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
}

advantage:

Lazy loading, which instantiates objects only when needed, is a strategy of sacrificing time for space, which can effectively solve the problem of hungry man wasting memory.

For lazy loading, write before SpringBoot's automatic assembly principle, custom starter and spi mechanism are all caught In, the spi mechanism in the JDK also uses the lazy loading mode. The ServiceLoader uses a lazyitterer internally, and the lazyitterer implements the Iterator. Its hasNext() method will find the next service implementation class, and the implementation class will be instantiated by calling the next() method with reflection to play the role of lazy loading.

Disadvantages:

Thread is unsafe, that is, in the case of multithreading, it is easy to instantiate multiple objects by multiple threads, which violates the principle of "single instance"

Thread safe lazy (non DCL)

Solve the problem of lazy thread insecurity

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

advantage:

The implementation is simple. Directly add a synchronization lock to the getInstance() method.

Disadvantages:

The granularity of locks is very coarse. After creation, multiple threads do not need to lock to obtain singleton objects at the same time, so the performance of non DCL mode is low.

Thread safe lazy (DCL)

Reduce the granularity of locks in non DCL mode

public class SingletonDCL {

    private volatile static SingletonDCL instance;

    private SingletonDCL() {
    }

    public static SingletonDCL getInstance() {
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }

}

Readers who are beginning to learn may have the following questions:

  Why check if it is null twice?

The original idea was to use the non DCL mode as an example, but it was too inefficient. We should narrow the scope of locks.

In the singleton mode, a singleton is required, and new SingletonDCL() can only be executed once. Therefore, the following method is preliminarily considered:

    public static SingletonDCL getInstance() {
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                    //Some time-consuming operations
                    instance = new SingletonDCL();
            }
        }
        return instance;
    }


However, there is a problem. After thread 1 and thread 2 judge that instance is null at the same time, then thread 1 gets the lock, creates a singleton object and releases the lock. After thread 2 gets the lock, it creates a singleton object.

At this time, thread 1 and thread 2 get two different objects, which violates the singleton principle.

Therefore, after obtaining the lock, you need to perform a null check again.

Why use volatile to modify a singleton variable?

This code, instance = new SingletonDCL(), is actually divided into three instructions at the virtual machine level:

  • Allocating memory space for instance is equivalent to opening up a space in the heap
  • Instantiating instance is equivalent to placing the instantiated SingletonDCL object in the space opened up in the previous step. All instance variables have been initialized and given the specified value
  • Point the instance variable reference to the first address of the space created in the first step

However, due to some optimizations made by the virtual machine, the instruction reordering may change from 1 - > 2 - > 3 to 1 - > 3 - > 2. This reordering will not have any problems in the case of single thread, but in the case of multi thread, the following problems may occur:

After thread 1 obtains the lock, it executes instance = new SingletonDCL(). At this time, due to the instruction reordering of the virtual machine, step 1 is carried out to open up memory space, and then step 3 is carried out. Instance points to the first address of the space, and step 2 has not been executed in time. At this time, thread 2 happens to execute getInstance method, and the outermost layer judges that instance is not null (instance has pointed to a certain address, so it is not null). The singleton object is returned directly. At this time, thread 2 gets an incomplete singleton object.

Therefore, volatile is used to modify the singleton variable to avoid instruction reordering. The principle analysis of volatile keyword will be covered in another chapter.

Static inner class

Lazy loading mode with manual locking is not required

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

}

advantage:

When only some properties of the external class are called, only the external class will be loaded, and the internal class (whether static or non static internal class) will not be loaded. The internal class will be loaded only when the properties using the internal class are displayed.

In other words, when using only the Singleton class, the SingletonHolder internal class will not be loaded, nor will the instance variable in the SingletonHolder internal class be initialized, which plays a role of lazy loading.

When a singleton object is used, the static properties make use of the class loading mechanism to ensure thread safety.

In addition, it is worth noting that when you directly use the properties of the static internal class, you will also load the external class, but the static internal class does not actually depend on the external class.

When using a non static internal class, you need to create an external class object first. Because the non static internal class will implicitly hold a strong reference to the external class, which is reflected in the fact that the constructor needs to pass in the external class object. That is, the non static internal class depends on the external class.

enumeration

The best practice of singleton is also advocated by Josh Bloch, author of Effective Java

public enum EnumSingleton {
    //Simulate the data in a single example
    INSTANCE(new Object());

    private Object data;

    EnumSingleton(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }
}

You can get the singleton object by calling EnumSingleton.INSTANCE in the external class.

There is no way to see directly. Compile the file with javac EnumSingleton.java, and then view the decompiled content with javap -p EnumSingleton.class

Compiled from "EnumSingleton.java"
public final class com.yang.ym.testSingleton.EnumSingleton extends java.lang.Enum<com.yang.ym.testSingleton.EnumSingleton> {
  public static final com.yang.ym.testSingleton.EnumSingleton INSTANCE;
  private java.lang.Object data;
  private static final com.yang.ym.testSingleton.EnumSingleton[] $VALUES;
  public static com.yang.ym.testSingleton.EnumSingleton[] values();
  public static com.yang.ym.testSingleton.EnumSingleton valueOf(java.lang.String);
  private com.yang.ym.testSingleton.EnumSingleton(java.lang.Object);
  public java.lang.Object getData();
  static {};
}

We can't see the method body. We change the xjad tool and download the address http://files.blogjava.net/96sd2/XJad2.2.rar

The decompiled contents are as follows:

public final class EnumSingleton extends Enum
{

	public static final EnumSingleton INSTANCE;
	private Object data;
	private static final EnumSingleton $VALUES[];

	public static EnumSingleton[] values()
	{
		return (EnumSingleton[])$VALUES.clone();
	}

	public static EnumSingleton valueOf(String s)
	{
		return (EnumSingleton)Enum.valueOf(EnumSingleton, s);
	}

	private EnumSingleton(String s, int i, Object obj)
	{
		super(s, i);
		data = obj;
	}

	public Object getData()
	{
		return data;
	}

	static 
	{
		INSTANCE = new EnumSingleton("INSTANCE", 0, new Object());
		$VALUES = (new EnumSingleton[] {
			INSTANCE
		});
	}
}

It can be seen that after compiling enum class EnumSingleton, a constructor will be generated, and the constructor will be used to instantiate the enumeration item in the static code block.

When the EnumSingleton enumeration class is loaded, the execution of static code blocks will be triggered in the initialization stage. Therefore, the enumeration class is thread safe and non lazy loading mode.

3, Destroy singleton mode

For singleton mode, a good implementation should try to ensure thread safety, reflection safety and serialization safety.

For thread safety, it means that only one thread can create a singleton object under multiple threads, and all threads can only obtain the same complete singleton object.

For reflection security, it means that the reflection mechanism cannot be used to break through the private constructor, so as to avoid generating multiple objects.

For serialization security, it means that a new object cannot be generated by deserialization.

Destruction of single case by reflection mechanism

Reflection destruction hungry Han style

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //Gets a private constructor without parameters
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        //Set private variables that can be accessed
        constructor.setAccessible(true);
        //Instantiate an object using a private constructor
        Singleton singleton = constructor.newInstance();
        System.out.println(singleton == Singleton.getInstance());//false
    }

At this point, using reflection makes it easy to create new objects, which violates the singleton principle.

Hungry Han style to ensure reflection safety

When a class is loaded, a singleton object will be created. Once the singleton object is not empty, the constructor can throw an exception directly.

The reconstructed hungry Han style is as follows:

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
        if (instance != null) {
            throw new RuntimeException("can not create singleton");
        }
    }

    public static Singleton getInstance() {
        return instance;
    }

}

After using reflection, an exception is thrown to refuse to create a new object.

Static inner classes ensure reflection security

In fact, it is also modified in the same way:

public class Singleton {
    private Singleton() {
        if (SingletonHolder.instance != null) {
            throw new RuntimeException("can not create singleton");
        }
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

}

When the constructor is called with reflection, the inner class loading will be triggered when null judgment is performed, so that instance is not empty and an exception will be thrown.

When Singleton.getInstance() is used normally, the loading of the internal class is triggered and the constructor will also enter. However, the internal class has been loaded at this time, so the instance is still empty and can be instantiated.

Reflection on lazy style to ensure reflection safety

For lazy style, if you do the same, you can't guarantee the safety of reflection. I don't have any ideas about specific solutions. If you know, you can leave a message below.

Reflection destruction enumeration class

It can be seen from the decompilation enumeration class in the previous section that EnumSingleton does not have a parameterless constructor, but there is a parameterless constructor. Let's modify the code:

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //Gets a private constructor with parameters
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class, Object.class);
        //Set private variables that can be accessed
        constructor.setAccessible(true);
        //Instantiate an object using a private constructor
        EnumSingleton singleton = constructor.newInstance();

        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println(singleton == instance);
    }

The operation will directly report an error

  Prompt that the newInstance line throws an exception and enters the method

If the current is an enumeration type, an exception is thrown directly. Thus, enumeration has natural reflection security properties.

Destroy singleton by serialization mechanism

When an object is serialized into the text and then deserialized from the text, the object obtained after deserialization may be reallocated memory, that is, a new object will be created.

Serialization destroys non enumerations

It is worth noting that hungry Han style needs to implement the Serializable interface first.

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton instance = Singleton.getInstance();
        ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("enum.txt"));
        ops.writeObject(instance);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enum.txt"));
        Object object = ois.readObject();
        System.out.println(instance == object);//false
    }

It seems that after deserialization, you do get a new object.

Let's go to the readObject to see what's done internally

The core method is   readObject0, when deserializing a TC_ Object (this identifier will be written to the beginning of the text when writing object), and readOrdinaryObject will be called

Then enter the readOrdinaryObject

  Isinstantable is used to check whether a class can be instantiated. It is certainly supported at present, so it will use newInstance reflection to create objects.

It is worth noting that the newInstance does not call the constructor of the singleton class, but the constructor of the Object. Therefore, the current Object will be filled with the data in the text next.

For hungry, lazy, or static inner classes, serialization creates new objects, breaking the singleton pattern.

So, what are the ways to avoid it?

Non enumeration ensures serialization security

In fact, the answer is hidden under isinstantable

If the current singleton class has a readResolve method, it will enter the invokereaderresolve method and return the object as the object returned by the final readObject.

The object returned by this method is the object returned by the readResolve method.

Directly add the readResolve method to the singleton class to return the current object or the object in the static internal class.

    public Object readResolve() {
        return instance;
    }

The readResolve method allows the class to replace the object read from the stream and freely control the object obtained by deserialization.

Serialization destroy enumeration

Modify the test method and run it directly.

Unexpectedly, true is returned directly, indicating that the enumeration class can ensure serialization security.

Enter the writeObject method first, and its internal core method is writeObject0

Then the type of serialization is judged

Enter writeEnum

It is found that the type, the fully qualified name of the current enumeration class, the serialVersionUID version number and the name of the enumeration item will eventually be written to the text.

writeObject ends here. Now look at the readObject method. The core method is readObject0

First get the type from the text, and then process it separately

Enter readEnum

Discovery is to directly use Enum.valueOf((Class)cl, name) to find the enumeration item named name in the cl enumeration class, that is, it is just a search without creating a new instance.

Therefore, enumeration classes have the nature of serialization security.

Posted by grudz on Wed, 06 Oct 2021 09:10:38 -0700