Do you know the seven ways to write singleton mode?

Hello, I'm Sanyi. Let's take a test: "how to write a single case of single case mode?"

"Isn't it the privatization of construction methods?"

”Yeah, yeah There are seven ways to write singleton mode, you know? “

Back to business

Singleton Pattern is the simplest design pattern.

An idiom is used to describe the single case model - "there are no two days in the world and no two masters in the country".

What does it mean? That is, the current process ensures that there is only one global instance of a class.

What are the benefits of singleton mode? [1]

  • Singleton mode has only one instance in memory, which reduces memory expenditure
  • Singleton mode only generates one instance, so it reduces the performance overhead of the system
  • Singleton mode can avoid multiple occupation of resources
  • The singleton mode can set global access points in the system

Is the singleton mode silver bullet? Does it have any disadvantages?

  • Singleton mode generally has no interface, so it is difficult to expand
  • Singleton mode is not conducive to testing
  • The single case mode conflicts with the principle of single responsibility

When should I use singleton mode?

  • Environment that requires a unique serial number to be generated
  • You need a shared access point or shared data throughout the project
  • Creating an object consumes too many resources
  • An environment where a large number of static constants and static methods (such as tool classes) need to be defined

Next, entering today's topic, let's take a look at the seven ways to write the singleton mode!

1. Hungry Han (thread safe) ⭐

public class Singleton_1 {

    private static Singleton_1 instance=new Singleton_1();

    private Singleton_1() {
    }

    public static Singleton_1 getInstance() {
        return instance;
    }

}

Hungry Han style, just like its name, is hungry. It is initialized directly when it is defined.

Because instance is a static variable, it will be instantiated when the class is loaded. There is no thread safety problem.

This method is not lazy loading. Whether our program will be used or not, it will be initialized at the beginning of program startup.

So we have the next way 👇

2. Lazy (thread unsafe) ⭐

public class Singleton_2 {

    private static Singleton_2 instance;

    private Singleton_2() {
    }

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

}

What is the lazy type? It is loaded only when it is used, which realizes the lazy loading of our minds.

But!

It introduces new problems? What problems? Thread safety.

It is also clear in the picture that there may be such problems in the case of multithreading:

A thread judges instance==null and starts initializing the object;

Before initializing the object, another thread accesses and judges instance==null to create the object.

The final result is the instantiation of two Singleton objects.

This does not meet the requirements of our single example? What should we do?

3. Lazy (locked)

public class Singleton_3 {

    private static Singleton_3 instance;

    private Singleton_3() {
    }

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

The most direct way is to lock it directly!

However, in this way, all accesses need to obtain locks, which leads to a waste of resources.

Then what shall I do?

4. Lazy type (double check lock) ⭐

public class Singleton_4 {
    //volatile modifier to prevent instruction rearrangement
    private static volatile Singleton_4 instance;

    private Singleton_4() {
    }

    public static Singleton_4 getInstance() {
        //First, check whether the instance exists
        if (instance == null) {
            //Synchronization block
            synchronized (Singleton_4.class) {
                //The second verification is to check whether the instance exists. If it does not exist, the instance is really created
                if (instance == null) {
                    instance = new Singleton_4();
                }
            }
        }
        return instance;
    }

}

This is a recommended double check lock.

Where is its progress?

We add synchronized to the method. Generally, the access is unlocked. It is locked only when instance==null.

At the same time, let's look at some key issues.

  • First, let's look at the first question, why double check?

Think about it, if you don't double check.

If two threads call the getInstance method together and both pass the first judgment instance==null, then the first thread obtains the lock, instantiates the instance, and then releases the lock. Then the second thread obtains the thread and instantiates the instance immediately. This does not meet the requirements of our single example.

Next, let's look at the second question, why modify instance with volatile?

We may know that the answer is to prevent instruction rearrangement.

What does this rearrangement refer to? It refers to instance = new Singleton(). We feel that it is an instantiated object of one-step operation. In fact, for JVM instructions, it is divided into three steps:

  1. Allocate memory space
  2. Initialize object
  3. Point the object to the memory space just allocated

Some compilers may reorder the second and third steps for performance optimization. The order is:

  1. Allocate memory space
  2. Point the object to the memory space just allocated
  3. Initialize object

So what happens if volatile is not used to prevent instruction rearrangement?

In this case, thread B accesses instance at T7 to access an object whose initialization is not completed.

Therefore, you need to add the keyword volatile before instance.

  • After using volatile keyword, order can be guaranteed and instruction reordering is prohibited;
  • volatile also ensures visibility, and the Java memory model ensures that the variable values seen by all threads are consistent.

5. Singleton mode (static inner class)

public class Singleton_5 {

    private Singleton_5() {
    }

    private static class InnerSingleton {
        private static final Singleton_5 instance = new Singleton_5();
    }

    public static Singleton_5 getInstance() {
        return InnerSingleton.instance;
    }
}

Static internal class is a further writing method, which can not only realize lazy loading and thread safety, but also maintain the ability of instruction optimization.

The Singleton class will not be instantiated immediately when it is loaded. When instantiation is needed, the static inner class InnerSingleton class will be loaded by calling getInstance method, so as to complete the instantiation of Singleton.

The static properties of the class will only be initialized when the class is loaded for the first time. At the same time, the class loading process is mutually exclusive. The JVM helps us ensure thread safety.

6. Singleton mode (CAS)

public class Singleton_6 {

    private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<Singleton_6>();

    private Singleton_6() {
    }

    public static final Singleton_6 getInstance() {
        //wait for
        while (true) {
            Singleton_6 instance = INSTANCE.get();
            if (null == instance) {
                INSTANCE.compareAndSet(null, new Singleton_6());
            }
            return INSTANCE.get();
        }
    }
}

This CAS single instance mode is a variant of lazy direct locking. Synchronized is a pessimistic lock, while CAS is an optimistic lock, which is more lightweight.

Of course, this writing method is also rare. CAS is busy, which may cause a waste of CPU resources.

7. Singleton mode (enumeration)

public enum Singleton_7 {

    //Define an enumeration that represents an instance of Singleton
    INSTANCE;
    public void anyMethod(){
        System.out.println("do any thing");
    }
}

Call method:

    @Test
    void anyMethod() {
        Singleton_7.INSTANCE.anyMethod();
    }

Effective Java is a method recommended by the author, which is very concise.

However, this method solves the most important problems: thread safety, serialization, and single instance.

summary

From the perspective of use, if lazy loading is not required, it's OK to directly use hungry Chinese style; if lazy loading is required, you can consider static internal classes or try enumeration.

From the perspective of interview, lazy type, hungry type and double check lock hungry type are the key points. The double check lock method must know where the instruction rearrangement is and what problems will be caused.

Tags: Java

Posted on Fri, 19 Nov 2021 08:16:08 -0500 by callisto11