当前位置:网站首页>A thorough understanding of singleton

A thorough understanding of singleton

2022-06-22 14:41:00 happysnaker

The article has been included in my warehouse :Java Share your study notes with free books

Design motivation

As its name suggests , The singleton pattern ensures that a class has only one instance , So why do you need to design a singleton pattern ?

For some classes , Only one instance is important , For example, a computer should have only one file system , Manufacturers should not configure a computer with two file systems ; An application should have a dedicated log object , Instead of writing here and there for a while ; There is often only one thread pool in a program , Threads are managed by a thread pool , Instead of using multiple thread pools , That makes threads messy and difficult to maintain ; stay Abstract factory in , Only one factory class should exist …

Such requirements , We all need to ensure that a class has only one instance , At this point, you can use the singleton mode .

Design

We must prevent users from instantiating multiple objects , The solution is to let the class save its only instance , And hide the constructor , And expose a specific static method to return a unique instance , In this way, users will not be able to instantiate multiple objects by themselves, but can only obtain unique instances through static methods exposed by classes .

Code example

Suppose the requirements are as follows : The same log object is required in the global state of an application .

One 、 The sluggard model

Lazy mode does not initialize instances directly , Instead, wait until the instance is used before initializing it , Avoid unnecessary waste of resources .

// Log file class 
class LogFile {
    
    // The only example 
    private static LogFile logFile = null;

    // The construction method is hidden from the outside 
    private LogFile(){
    };
    
    // Methods of exposure 
    public static LogFile getInstance() {
    
        // The sluggard model 
        if (logFile == null) {
    
            logFile = new LogFile();
        }
        return logFile;
    }
}

public class Test {
    
    public static void main(String[] args) {
    
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Output true, The same object is generated 
    }
}

The code here works well in a single thread ,logFile Resources belonging to the critical zone , So this is not thread safe , The initial example is null, Threads A After execution if After the judgment sentence, execute logFile = new LogFile() Before being scheduled to the thread B, The thread B The instance you see is also empty , because A It's not initialized yet , So threads B Initialize instance , When returning to the thread A when , Threads A Construction will continue logFile The sentence of , here logFile It has been initialized twice , they A And B The obtained instance is not the same .

A simple solution is to lock them :

public static LogFile getInstance() {
    
    synchronized (LogFile.class) {
    
        // The sluggard model 
        if (logFile == null) {
    
            logFile = new LogFile();
        }
    }
    return logFile;
}

But the efficiency of locking is too low , It would be better to adopt the starving Han style , Because the instant when logFile Isn't empty , Multiple threads must also queue to get instances , In fact, there is no need to queue up , When logFile Isn't empty , Multiple threads should be able to get logFile example , Because they just read the instance and do not change it , Shared reads are thread safe .

A better solution is to use double check locking :

public static LogFile getInstance() {
    
    if (logFile == null) {
    
        synchronized (LogFile.class) {
    
            // The sluggard model 
            if (logFile == null) {
    
                logFile = new LogFile();
            }
        }
    }
    return logFile;
}

Judge by adding a weight to the outer layer , We have solved the problems mentioned above , Now the code is efficient enough —— Locking is only involved at the very beginning .

Be careful , The above code is still thread unsafe , If you want thread safety , We have to logFile Instance declaration plus volatile keyword , namely :

private volatile static LogFile logFile;

To understand this , We must understand new The process of an object , The general process is as follows :

  1. Apply for memory space , Use default initialization for fields in the space ( The object is null).
  2. Call the constructor of the class , To initialize ( The object is null).
  3. The return address ( After execution, the object is not null).

If not volatile keyword ,Java The virtual machine may have instruction rearrangement under the premise of ensuring serialization , That is, the virtual machine may execute the first step 3 Proceed to step 2 Step ( Relatively rare ), When initializing objects, the virtual machine only considers the single thread situation , At this point, the instruction rearrangement does not affect the single thread operation , So in order to speed up , Instruction rearrangement is possible .

From the perspective of multithreading , If an instruction rearrangement occurs , Threads A stay new Object to execute the first part and then the third step , The object is no longer null 了 , But the object hasn't been constructed yet , Although this time the thread A And hold the lock , But this pair of threads B It has no effect —— Threads B The object of intrusion discovery is not null Instead, take away an object instance that has not been completely constructed —— You will not apply for a lock through the first level of judgment .

add volatile Keywords guarantee visibility and prevent instruction rearrangement .

But from a problem-solving point of view , We have a better solution —— Static inner class :

// Log file class 
class LogFile {
    
    // The instance is kept by the static inner class 
    private static class LazyHolder {
    
        private static LogFile logFile = new LogFile();
    }

    // The construction method is hidden from the outside 
    private LogFile(){
    };

    // Methods of exposure 
    public static LogFile getInstance() {
    
        return LazyHolder.logFile;
    }
}

public class Test {
    
    public static void main(String[] args) {
    
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Output true, The same object is generated 
    }
}

Static inner classes work best , Static inner classes are loaded only when their member variables or methods are referenced , That is to say, the instance will be initialized only when we access the class for the first time , We delegate the instance to the static inner class to help initialize , The loading of static inner classes by virtual machines is thread safe , We avoid using locking mechanisms to delegate to virtual machines , This efficiency is very high .

Lazy style can avoid the generation of useless garbage objects —— It is initialized only when they are used , But we must also write more code to ensure its security , If a class is not very common , Using lazy style can save resources to a certain extent .

Two 、 Hungry Chinese style

The starving mode initializes the singleton when it is loaded , Enable the instance to be initialized when the user obtains it .

// Log file class 
class LogFile {
    
    // The only example 
    private static LogFile logFile = new LogFile();

    // The construction method is hidden from the outside 
    private LogFile(){
    };

    // Methods of exposure 
    public static LogFile getInstance() {
    
        return logFile;
    }
}

public class Test {
    
    public static void main(String[] args) {
    
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Output true, The same object is generated 
    }
}

Starved Chinese is thread safe , because logFile Has been initially completed , Therefore, the hungry type is more efficient than the lazy type , But at the same time , If the instance is not used globally , The hungry man model will generate garbage and consume resources .

Summary of advantages and disadvantages

The main advantages :

  1. Provides controlled access to unique instances .

  2. There is only one object in the system , Save system resources .

  3. Singleton mode allows a variable number of instances .

Main drawback :

  1. Poor scalability .

  2. Singleton class , Too much responsibility , To a certain extent " Principle of single responsibility ".

  3. Abusing a singleton will cause some negative problems , For example, in order to save resources, the database connection pool object is designed as a singleton class , Connection pool overflow may occur due to too many programs sharing connection pool objects ( Everyone uses a pool , Maybe the pool can't bear it ), If the instantiated object is not used for a long time, the system will be regarded as garbage object and recycled , This will result in the loss of object state .

原网站

版权声明
本文为[happysnaker]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/173/202206221314158059.html