[JavaEE] Multi-threading case-single case mode

Insert image description here

1 Introduction

The singleton pattern is the most commonly tested design pattern in our interviews . What are design patterns?

A design pattern is, in computer science, a description of solutions to recurring problems in object-oriented design. It is a summary of code design experience that has been used repeatedly, is known to most people, and has been classified and cataloged.

The purpose of design patterns is to reuse code, make the code easier to understand by others, and improve the reliability of the code. They usually describe a group of classes and objects that interact closely with each other, providing a common language for discussing software design, so that the design experience of skilled designers can be mastered by beginners and other designers. Additionally, design patterns provide goals for software refactoring.

Design patterns can be divided into the following three categories based on their purpose:

  1. Creational patterns: Mainly used for creating objects, these design patterns provide a way to hide the creation logic while creating objects, instead of directly instantiating objects using the new operator.
  2. Structural pattern: mainly used to deal with the combination of classes and objects.
  3. Behavioral pattern: Mainly used to describe how classes or objects interact and how to assign responsibilities.

In addition, according to the scope, that is, whether the pattern mainly deals with the relationship between classes or the relationship between objects, it can be divided into two types: class pattern and object pattern.

2. What is the singleton pattern?

The singleton pattern ensures that only one instance of a class exists in the program, without creating multiple instances. Just like a person can only have one partner, but not multiple partners.

3. How to implement singleton pattern

Although we can manually control the existence of only one instance of this class, we humans are the most untrustworthy creatures, so we need to use computers to constrain us. When we want to create multiple instances, the compiler needs to respond accordingly: throw an exception or directly end the process, etc.

There are two ways to implement the singleton pattern in Java:

  1. Hungry mode
  2. lazy mode

3.1 Hungry Man Mode

To ensure that there is only one instance of a certain class, a good way is to create an instance when we define the class, and this instance is unique. When this class is created, it is not allowed to be created again. An instance of the class.

class Singleton {
    
    
	//定义类的时候就创建一个唯一的实例
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance() {
    
    
        return instance;
    }
}

Because instances of this class cannot be created after leaving this class, and we need to obtain the instance created when the class was defined, we can use a static getInstance method to obtain this unique instance.

Although we have created this unique instance, how can we ensure that no more instances can be created after this class is created?

We all know that every time an instance is created, the constructor of the class will be called (if you do not implement the constructor, the compiler will create a parameterless constructor for you by default), so we can start with this constructor. : Change the constructor to a private constructor. The creation will only be successful when an instance is created in this class. After leaving this class, if a second instance is created, because the constructor is private, it will be created successfully. Creation will fail.

class Singleton {
    
    
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
    
    
        return instance;
    }
    
    private Singleton() {
    
    }
}

Let's see what happens when we want to create multiple instances:

Insert image description here
When we write code, errors will be reported in red. Then we run again.

Insert image description here
Therefore, it is possible to successfully implement the singleton mode through the above hungry man mode. Then let's take a look at how the lazy man mode implements the singleton mode.

3.2 Lazy mode

Why is the previous one called Hungry Man Mode? Because when the Hungry Pattern defines a class and creates a static instance, we all know that static member variables will be created when the class is loaded. This will cause this instance to be created regardless of whether we use it or not, which will cause a waste of memory and time. Our lazy mode solves this problem very well. When defining a class, we do not create this instance first, but define this instance first, assign this instance to null, and when calling the getInstance method, judge this Whether the instance is null. If it is null, create the instance, apply for space and initialize it. If it is not null, it will return directly.

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

Insert image description here
Insert image description here

But is this the end? Of course not. Since it is a multi-threaded case, we must consider thread safety issues. Then let's take a look at how to solve thread safety issues encountered in singleton mode.

4. Solve the thread safety issues encountered in singleton mode

Do both hungry man mode and lazy man mode cause thread insecurity problems? No, because in the hungry mode there is only judgment on the variable and no modification operation, but in the lazy mode, after judging whether the instance is null, the instance will be modified. If there are judgment and modification operations in the thread, it is often Thread insecurity will occur, so only lazy mode will cause thread insecurity.

Insert image description here

4.1 Locking

In order to solve the problem of thread insecurity during the judgment and modification process, locking needs to be performed during this process.

class Singleton2 {
    
    
    private static Singleton2 instance = null;

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

        return instance;
    }

    private Singleton2() {
    
    }
}

Although we have locked during this process, this locking process does not require locking every time the getInstance method is called. If locking is frequent, then our code will not be efficient. Locking is only required when the getInstance method is called for the first time, so how can we optimize this frequent locking problem?

4.2 Add a judgment to solve the problem of frequent locking

class Singleton2 {
    
    
    private static Singleton2 instance = null;

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

        return instance;
    }

    private Singleton2() {
    
    }
}

When adding a judgment, someone may ask, I used two identical judgments to create an instance, so doesn't this judgment seem redundant? No more than, these two judgments are not redundant at all.

  • The first judgment is to determine whether locking is needed to avoid frequent locking.
  • The second judgment is to judge whether an instance needs to be created

When the instance is no longer null, then because of the first judgment, the lock will not be performed, but the instance will be returned directly.

4.2 Solve the thread unsafe problem caused by instruction reordering

Only the above two optimizations are not enough. We all know that there are thread insecurity problems and instruction reordering problems. The process of creating an instance can be broken down into three steps:

  1. Apply for space in memory
  2. Call the constructor to initialize the memory
  3. Assign this memory to instance

If instructions are reordered during the creation of an instance, and the order of execution of thread t1 should be 1, 2, 3, but it is reordered to 1, 3, 2, then when thread t2 and thread t1 execute concurrently Sometimes, an uninitialized reference will be returned, which will have serious consequences.

Insert image description here
Therefore, in order to solve the thread insecurity problem caused by instruction reordering, we need to use volatile to ensure the visibility of memory and prevent instruction reordering from occurring.

class Singleton2 {
    
    
    private volatile static Singleton2 instance = null;

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

        return instance;
    }

    private Singleton2() {
    
    }
}

Insert image description here

With these three optimizations, the safety of the singleton mode is truly guaranteed.

Guess you like

Origin blog.csdn.net/m0_73888323/article/details/132841449