This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

1. Introduction

What is the singleton pattern?

A singleton is a design pattern in which objects are created in memory only once. The singleton pattern allows a program to create only one object in memory, sharing it with all the places that need to be called, to prevent memory from exploding when the same object is used multiple times in the program.

Classification of 2.

Singleton patterns can be divided into two types:

  • Lazy: Create the singleton object only when you really need to use it
  • Hunger-hungry: This singleton is created when the class is loaded, waiting to be used by the program

Note: The constructors for both of the above methods of creating singletons are privatized.

2.1 Creating a Singleton

2.1.1 Creating a Singleton In lazy Mode

In the case of single thread, lazy singleton can be used to determine whether the object is not empty (instantiated), if not empty, it indicates that the singleton class object has been instantiated, directly return the instantiated singleton object; If empty, the singleton has not been instantiated yet. Instantiate first and return the object.

package single;

public class Lazy {
    private  static Lazy lazy;

    private Lazy(a){}public static Lazy getInstance(a) {
        if(lazy == null) {
            lazy = new Lazy();
        }
        returnlazy; }}Copy the code

This lazy approach to creating singletons is correct in the single-threaded case, but there are problems with multi-threading, which we will address later. Let’s take a look at the hanhan-style way to create a singleton.

2.1.2 Creating a Singleton In Hungry Mode

The hunger-style way to create a singleton is to create it as soon as the class is loaded, rather than waiting until it is used.

package single;

public class Hungry {
    private static Hungry hungry = new Hungry();
    
	private Hungry(a) {}
    
    public static Hungry getInstance(a) {
        returnhungry; }}Copy the code

2.1.3 Optimizing lazy Creation of Singletons

Let’s review the lazy approach to creating singletons above

package single;

public class Lazy {
    private  static Lazy lazy;

    private Lazy(a){}public static Lazy getInstance(a) {
        if(lazy == null) {
            lazy = new Lazy();
        }
        returnlazy; }}Copy the code

This is problematic in the case of multiple threads, if both threads enter the if(lazy == null) if assertion, then two objects will be created, which does not meet the singleton requirement, so we need to solve the thread safety problem first.

The simplest method is to place a lock on the method as follows:

package single;

public class Lazy {
    private  static Lazy lazy;

    private Lazy(a){}public synchronized static Lazy getInstance(a) {
        if(lazy == null) {
            lazy = new Lazy();
        }
        returnlazy; }}Copy the code

This is a good way to solve the problem of thread safety, but it has poor concurrency performance.

Let’s see if we can optimize performance in another way that locks the object regardless of whether it has already been instantiated. We can try optimization like this: when we do not instantiate the object, we can lock the creation, but when we have instantiated the object, we just return the instantiated object. As follows:

package single;

public class Lazy {
    private  static Lazy lazy;

    private Lazy(a){}public static Lazy getInstance(a) {
        if(lazy == null) {
            synchronized (Lazy.class){
                if(lazy == null){
                    lazy = newLazy(); }}}returnlazy; }}Copy the code

The code above is a complete solution to thread safety and performance inefficiencies. The principle is very simple, but the above method requires Double nulling and locking the class. This lazy method is also known as Double Check + Lock.

But the lazy creation singleton pattern above is not the perfect lazy creation singleton pattern, why? Let’s learn.

Let’s start with command reordering:

What is order reordering?

Instruction reordering is the ability of the JVM to execute statements in a non-programmed order to maximize program performance while ensuring that the final result is correct.

In a program, instruction rearrangement occurs if there is no dependency between two instructions. For example, if there is no dependency between a = 1 and y = 2, instruction rearrangement will occur.

For example, a = 1, y = a + 1 are dependent on each other, so instruction rearrangement will not occur.

Under a single thread, instruction reordering actually has no effect on the result of the program;

However, in the case of multi-threading, instruction rearrangement will affect the execution result of the program. Let’s take an example to illustrate:

package single;

public class Demo02 {
    private int a = 0;
    private boolean flag = false;

    public void method01(a){
        a = 1;
        flag = true;
    }

    public void metgod02(a){
        if(flag){
            a = a + 5; System.out.println(a); }}}Copy the code

When thread 1 executes method01(), the two statements are not dependent on each other and instructions can be rearranged, but if thread 2 executes method02(), thread 2 May output 5 or 6 or nothing at all.

The volatile keyword prefixes variables to avoid instruction reordering (why? I haven’t learned yet.

This led to the final version of the lazy creation singleton pattern:

package single;

public class Lazy {
    private volatile static Lazy lazy;

    private Lazy(a){}public static Lazy getInstance(a) {
        if(lazy == null) {
            synchronized (Lazy.class){
                if(lazy == null){
                    lazy = newLazy(); }}}returnlazy; }}Copy the code

3. Break hungry singleton and lazy singleton

Whether we create a singleton slack-style or hunger-style, we can use reflection to break the singleton pattern (producing multiple objects) as follows:

package single;


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Demo01 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // Get the explicit constructor for the class
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
        // You can access the class's private constructor, violent reflection
        constructor.setAccessible(true);
        // Use reflection to construct a new object
        Lazy lazy1 = constructor.newInstance();
        // Get the singleton in the normal wayLazy lazy2 = Lazy.getInstance(); System.out.println(lazy1 == lazy2); }}Copy the code

We can see that reflection can successfully destroy lazy singletons in the same way that hungry singletons can be destroyed.

4. Use enumerated classes to implement the singleton pattern

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum Single {
    SINGLE;
    Single(){

    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Single s1 = Single.SINGLE; Single s2 = Single.SINGLE; System.out.println(s1 == s2); }}Copy the code

Next, let’s try to crack it with reflection:

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum Single {
    SINGLE;
    Single(){

    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<Single> constructor = Single.class.getDeclaredConstructor();
        constructor.setAccessible(true); Single s3 = constructor.newInstance(); Single s4 = Single.SINGLE; System.out.println(s3 == s4); }}Copy the code

An exception is thrown when using reflection cracking.

What are the advantages of using enumerated classes to implement the singleton pattern?

  1. The code is much cleaner than slacker and hungrier code.

  2. Nothing extra needs to be done to keep the object simple and thread-safe

    One way to think about how enumerated classes create singletons is that when the program starts, Single’s empty parameter constructor is called, a Single object is instantiated, assigned to Single, and never instantiated again.

  3. Using enumerated classes prevents callers from breaking the singleton pattern by forcing multiple objects to be generated using reflection, serialization, and deserialization mechanisms.

5. To summarize

  1. There are two common implementations of the singleton pattern: lazy and hungry, but the perfect way is to use enumerated classes, which are clean, thread-safe, and protected from reflection, serialization, and deserialization.
  2. For the lazy: Double Check + Lock for thread safety and concurrency efficiency.
  3. For hangotype: it is created when the class is loaded, and there are no thread safety and concurrency efficiency issues.
  4. In the development, if the memory requirements are not high, we will use hungry Chinese writing method, because it is simple and not easy to make mistakes; Use lazy writing if memory requirements are high.
  5. The volatile keyword prevents instruction reordering.
  6. The most elegant way to implement the singleton pattern is to use enumerated classes. Not only is the code concise, but there are no thread-safety issues, and it is protected from being corrupted by reflection and serialization and deserialization.