Design pattern: Singleton pattern

Time:2021-3-21

One of Java design patterns — singleton pattern

Singleton mode

Singleton mode restricts the instance of class and ensures that only one instance of Java class exists in Java virtual machine.

Singleton classes must provide a global access to obtain instances of the class.

Singleton mode is used to log, drive objects, cache and thread pool.

Singleton design pattern is also used in other design patterns, such as abstract factory, builder, prototype, facade and so on.

Singleton patterns are also used in core Java, such as java.lang.Runtime , java.awt.Desktop

Java singleton mode

We have different ways to implement the singleton pattern, but they all share the following concepts.

  • Private constructor methods restrict the initialization of instances of a class from other classes.
  • Private static variables are the same as instances of this class.
  • The public static method returns the instance of the class, which is the global access point provided to external access to obtain the instance of the singleton class. In the following sections, we will learn about the different implementations of singleton pattern.

The common implementation methods are as follows

  • Hungry man style
  • Sluggard style
  • Volatile double check lock mechanism
  • Static inner class
  • Enumeration (NATURAL singleton)

Hungry man style

As the name suggests, starvation is to create an instance object when the class is referenced for the first time, regardless of whether it is needed or not. The code is as follows:

    public class Singleton {   
        private static Singleton singleton = new Singleton();
        private Singleton() {}
        public static Singleton getSignleton(){
            return singleton;
        }
    }

Pros and Cons: the advantage of this is that the code is simple, but it can’t delay loading. But most of the time we want to delay loading, so as to reduce the load, so we have the following lazy style;

Sluggard style

Single thread writing
This writing method is the simplest. It consists of a private constructor and a public static factory method. In the factory method, singleton is judged to be null. If it is null, it will be new. Finally, singleton object is returned.
This method can achieve delay loading, but it has a fatal weakness
Thread is not safe. If two threads call the getsingleton () method at the same time, it is very likely to cause repeated object creation.

public class Singleton {
   private static Singleton singleton = null;
   
   private Singleton(){}
   
   public static Singleton getSingleton() {
       if(singleton == null) {
         singleton = new Singleton();
       }
       return singleton;
   }
}

Thread safe writing
This writing method considers thread safety, and uses null judgment of singleton and new partsynchronizedLock. At the same time, we use thevolatileKeywords,Ensure its visibility to all threads and prohibit instruction reordering optimization. In this way, we can semantically ensure that the singleton mode is thread safe. Note that there is a small hole in the actual use, which will be written later.

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

Double check lock

Although the above writing method can run correctly, it is inefficient and cannot be applied in practice. Because every time the getsingleton () method is called, it must be queued in synchronized, and it is very rare to really meet the need for new. Therefore, there is a third way of writing

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

This writing method is called “double check lock”. As the name suggests, it is to check null twice in the getsingleton () method. It seems unnecessary, but in fact, it greatly improves the concurrency and performance. Why can concurrency be improved? As mentioned above, there are very few new cases in a single instance, and most of them are parallel read operations. Therefore, one more null check before locking can reduce the vast majority of locking operations and improve the efficiency of execution;

Design of double check lock mechanism

So, is this writing absolutely safe? As I said before, from a semantic point of view, there is no problem. But there are holes.

  • We need to look at this pit before we talk about itvolatileThis keyword. In fact, this keyword has two levels of semantics.
  • We are relatively familiar with the first layer of semantics, visibility. Visibility means that the modification of the variable in a thread is written back to main memory by work memory, so it will be immediately reflected in the read and write operations of other threads. By the way, working memory and main memory can be roughly understood as the cache and main memory in a computer,Working memory is exclusive to threads, and main memory is shared by threads
  • The second level semantics of volatile is to prevent instruction rearrangement. As you know, the code we write (especially the multithreaded code), due to compiler optimization, may be different from the order we write in the actual execution. The compiler only guarantees that the program execution result is the same as the source code, but does not guarantee that the order of the actual instructions is the same as the source code. This is not a problem in a single thread, but once multithreading is introduced, this disorder can cause serious problems. The volatile keyword can solve this problem semantically.

be careful,It is not until JDK1.5 that instruction rearrangement optimization is prohibited. In previous JDK, even if variables were declared volatile, the problem caused by reordering could not be completely avoided. Therefore, before JDK1.5, the singleton mode of double check lock cannot guarantee thread safety.

Static inner class

So, is there a simple way to delay loading and ensure thread safety? We can put singleton instances into a static inner class,In this way, the static instance will not create an object when the singleton class is loaded. Since the static internal class will only be loaded once, this writing method is thread safe

public class Singleton {

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

However, all of the implementations mentioned above have two common disadvantages:

  1. All need extra work(Serializable、transient、readResolve())Otherwise, a new instance will be created every time a serialized object instance is deserialized.
  2. Someone might use reflection to force a call to our private constructor (to avoid this, modify the constructor to throw an exception when creating a second instance).

Enumeration writing

Of course, there is a more elegant way to implement singleton mode, which is enumeration

public enum SingleEnum {
    NEW_INSTANCE {
        @Override
        protected void doSomething() {
            System.out.println ("--- business method call ---);
        }
    };

    SingleEnum() {
    }

    /**
     *Business method definition
     */
    protected abstract void doSomething();

    public static void main(String[] args) {
        SingleEnum.NEW_INSTANCE.doSomething();
    }
}

In addition to thread safety and preventing reflection from forcibly calling the constructor, enumeration also provides an automatic serialization mechanism to prevent the creation of new objects during deserialization. Therefore, effective Java recommends using enumeration to implement singleton as much as possible.

summary

Code is not written once and for all, only under certain conditions the most appropriate writing. In different platforms, different development environments (especially JDK version), there are different optimal solutions (or better solutions).
For example, enumeration is recommended in effective Java, but it is not recommended on Android platform. In this Android training, it is clearly pointed out that:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

Another example is the double check lock method, which can not be used before JDK1.5, but can be used on Android platform (generally Android is above JDK1.6, which not only corrects the semantic problem of volatile, but also adds a lot of lock optimization, which reduces the cost of multithreading synchronization).

Finally, no matter what plan you take, always keep in mind the three main points of the single example:

  • Thread safety
  • Delayed loading
  • Serialization and deserialization security

reference material

Effective Java (2nd Edition)
Deep understanding of Java virtual machine: advanced features and best practices of JVM (2nd Edition)