The motivation

As its name suggests, the singleton pattern guarantees only one instance of a class, so why design a singleton pattern?

For some classes, it is important to have only one instance. For example, a computer should have only one file system, and manufacturers should not configure two file systems for a computer. An application should have a dedicated logging object, not write here and there; There is usually only one thread pool in a program, and threads should be managed by one thread pool, rather than multiple thread pools, which can make threads messy and difficult to maintain. In an abstract factory, there should be only one concrete factory class,……

Such requirements require that there be only one instance of a class, in which case the singleton pattern can be used.

design

We must prevent the user from instantiating multiple objects. The solution is to have the class store its own unique instance, hide the constructor from the public, and expose a specific static method to return a unique instance, so that the user cannot instantiate multiple objects and can only get a unique instance from the static method exposed by the class.

Code sample

Assume the following requirements: the same log object is required in the global state of an application.

1. Slacker mode

Lazy mode does not initialize an instance directly, but waits until the instance is in use to avoid unnecessary waste of resources.

// Log file class
class LogFile {
    // Unique instance
    private static LogFile logFile = null;

    // the constructor is hidden from view
    private LogFile(a){};
    
    // External exposure method
    public static LogFile getInstance(a) {
        // Slacker mode
        if (logFile == null) {
            logFile = new LogFile();
        }
        returnlogFile; }}public class Test {
    public static void main(String[] args) {
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Print true to produce the same object}}Copy the code

This code works well in A single thread, logFile is A critical section resource, so it is not thread safe to write this way. The initial instance is null. Thread A is scheduled to thread B after executing an if determinate statement before executing logFile = new logFile (). Because A hasn’t initialized yet, thread B initializes the instance, and when it gets back to thread A, thread A will continue to execute the statement that constructed the logFile, so the logFile has been initialized twice, and they don’t have the same instance as THREAD B.

A simple solution is to lock them:

public static LogFile getInstance(a) {
    synchronized (LogFile.class) {
        // Slacker mode
        if (logFile == null) {
            logFile = newLogFile(); }}return logFile;
}
Copy the code

But lock this efficiency is too low, it is better to adopt the hungry, because instant when the logFile is not null, multiple threads must also queue up for instance, and in fact do not need to queue, when the logFile is not null, multiple threads should be able to access logFile instance at the same time, because they just read the instance and will not change, Shared reads are thread-safe.

A better solution is to use double-checked locking:

public static LogFile getInstance(a) {
    if (logFile == null) {
        synchronized (LogFile.class) {
            // Slacker mode
            if (logFile == null) {
                logFile = newLogFile(); }}}return logFile;
}
Copy the code

We solved the above problem by adding another judgment to the outer layer, and now the code is efficient enough — locking is only involved in the very beginning.

Note that the above code is still thread-safe. To be thread-safe, we must declare the logFile instance as volatile.

private volatile static LogFile logFile;
Copy the code

To understand this, we need to understand the process of creating an object, which is roughly as follows:

  1. Allocates memory space and initializes fields in the space by default (the object is null).
  2. Call the constructor of the class and initialize it (the object is null).
  3. Returns the address (the object is not null after execution).

Without volatile keyword, the Java virtual machine can happen on the premise of guarantee the serializable instruction rearrangement, namely virtual machine can perform step 3 again first step 2 (rare), initialize the object of virtual machine consider when only a single thread, the rearrangement of instructions will not affect the operation of the single thread, It is therefore possible for instructions to be rearranged to speed things up.

If you look at it from A multithreaded point of view, if there’s an instruction rearrangement, thread A does the first part of the new object and then does the third step, the object is no longer null, but the object is not yet constructed, even though thread A is still holding the lock, But this has no effect on thread B — thread B breaks in and finds that the object is not null and simply takes a partially constructed object instance — and does not apply the lock through the first level of judgment.

The volatile keyword ensures visibility and disallows instruction reordering.

But from a problem-solving point of view, we have a better solution — static inner classes:

// Log file class
class LogFile {
    // Give the instance to the static inner class
    private static class LazyHolder {
        private static LogFile logFile = new LogFile();
    }

    // the constructor is hidden from view
    private LogFile(a){};

    // External exposure method
    public static LogFile getInstance(a) {
        returnLazyHolder.logFile; }}public class Test {
    public static void main(String[] args) {
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Print true to produce the same object}}Copy the code

The effect of the static inner class is the best, only in the static inner class member variables or methods will be referenced when loading, that is to say, only when we first access class instance initialization is complete, we will be entrusted to help to initialize static inner class instance, virtual machine for loading of the static inner class is thread-safe, It was very efficient to avoid locking ourselves and delegating to the virtual machine.

Lazy avoids the creation of garbage objects — we only initialize them when they are in use, but we also have to write more code to make them secure. Using lazy can save resources if a class is not used very often.

Hungry

The Hanchian pattern initializes the singleton at load time so that the instance is already initialized by the time the user gets it.

// Log file class
class LogFile {
    // Unique instance
    private static LogFile logFile = new LogFile();

    // the constructor is hidden from view
    private LogFile(a){};

    // External exposure method
    public static LogFile getInstance(a) {
        returnlogFile; }}public class Test {
    public static void main(String[] args) {
        var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // Print true to produce the same object}}Copy the code

Hangry is thread-safe because the logFile has already been initialized, so it is more efficient than lazy, but at the same time, if the instance is not used globally, hangry will generate garbage and consume resources.

Summary of advantages and disadvantages

Main advantages:

  1. Provides controlled access to a unique instance.

  2. The system has only one object in memory, saving system resources.

  3. The singleton pattern can allow a variable number of instances.

Main disadvantages:

  1. Poor scalability.

  2. Singleton classes are too responsible and violate the “single responsibility principle” to some extent.

  3. Abuse singleton will bring some negative problems, such as in order to save resources will object database connection pool design for singleton class, object program too much could lead to a Shared connection pool and connection pool overflow (everyone with a pool, pool may be unable to), if instantiation objects for a long time it will be not system is considered garbage objects are recycled, This causes the object state to be lost.