27 - how many ways to implement a single example? What are their advantages and disadvantages?

Posted by vbracknell on Tue, 23 Jun 2020 10:58:01 +0200

Singleton pattern is one of the simplest design patterns in Java. It means that a class always has only one instance during its running. We call it singleton pattern. It is not only used in practical work, but also one of the most frequently tested questions in interview. Through the singleton mode, we can know the programming style of this person and whether the basic knowledge is firm.

Our interview questions in this lesson are: how many ways can a single example be realized? What are their advantages and disadvantages?

Typical answer

The implementation of the single case can be divided into the hungry man mode and the lazy man mode. As the name suggests, the hungry man mode is like a hungry man, and he has a certain sense of crisis. He will stock up food in advance, so that he can eat food directly after he is hungry. Corresponding to the program, it means that when the class is loaded, the initialization of a single instance will be carried out, and the single instance object can be directly used for later access.

The implementation code of starved Chinese mode is as follows:

public class Singleton {
    // Declare private objects
    private static Singleton instance = new Singleton();    
    // Get instance (singleton object)
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() {
    }
    // method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        // Call singleton object
        Singleton singleton = Singleton.getInstance();
        // Call method
        singleton.sayHi();
    }
}

The execution results of the above procedures are as follows:

Hi,Java.

From the above results, we can see that the singleton object has been successfully obtained and successfully executed the methods in the class. Its advantage is thread safety, because the singleton object has been initialized when the class is loaded. When the singleton object is called, it only assigns the already created object to the variable. Its disadvantage is that it may cause resource waste. If the singleton object is loaded by the class (the singleton object is created), but it has not been used, it will cause resource waste.

The lazy man mode is also known as the full man mode. As the name implies, he is relatively lazy. He only goes out to find food when he needs to eat, instead of preparing food as early as a hungry man. Corresponding to the program means that when you need to use an instance every time, you need to create and get an instance instead of creating the instance when the class is loaded.

The implementation code of lazy mode is as follows:

public class Singleton {
    // Declare private objects
    private static Singleton instance;
    // Get instance (singleton object)
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() {
    }
    // method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

The execution results of the above procedures are as follows:

Hi,Java.

From the above results, we can see that the singleton object has been successfully obtained and successfully executed the methods in the class. The advantage of singleton object is that it will not cause waste of resources, because the instantiated object will be created when it is called. Its disadvantage is that non thread is safe in multi-threaded environment, such as multiple threads executing to if at the same time At this time, the judgment results are uninitialized, then these threads will create n instances at the same time, which will lead to unexpected situations.

Examination point analysis

Using the singleton mode can reduce the memory overhead of the system and improve the running efficiency of the program, but if it is not used properly, it will cause the concurrency problem under multithreading. The starved man mode is the most direct way to realize the singleton mode, but it may cause a waste of system resources, so only answers that can not only ensure the thread safety, but also avoid the waste of system resources can completely conquer the interviewer.

The interview questions related to this knowledge point are as follows:

  • What is a double detection lock? Is it thread safe?
  • Is there any other way to implement the singleton?

Knowledge expansion

Double detection lock

In order to ensure the thread safety of lazy mode, the simplest way is to add synchronized (synchronous lock) decoration to the method of getting instance, as shown in the following code:

public class Singleton {
    // Declare private objects
    private static Singleton instance;
    // Get instance (singleton object)
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

This can make lazy mode thread safe, but because the whole method is surrounded by synchronized, it increases the synchronization cost and reduces the execution efficiency of the program.

In order to improve the execution efficiency of the program, we put synchronized into the method to reduce the code range decorated by the synchronization lock. The implementation code is as follows:

public class Singleton {
    // Declare private objects
    private static Singleton instance;
    // Get instance (singleton object)
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

Carefully you may find that the above code also has non thread safety problems. For example, when two threads execute the "if (instance == null) {" judgment at the same time, the result of the judgment is true, so they queue up to create new objects, which obviously does not meet our expectations. Thus, the famous Double Checked Lock (DCL) was born. The implementation code is as follows:

public class Singleton {
    // Declare private objects
    private static Singleton instance;
    // Get instance (singleton object)
    public static Singleton getInstance() {
        // First judgment
        if (instance == null) {
            synchronized (Singleton.class) {
                // Second judgment
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

The above code seems perfect, but there is a small problem that is not easy to be found. The problem lies in the line of new object code, that is, the line of instance = new Singleton(). This line of code seems to be an atomic operation, but it is not. This line of code will eventually be compiled into multiple assembly instructions. Its general execution process is as follows:

  • Allocate memory space to object instances;
  • Call the object's construction method and initialize the member field;
  • Point the instance object to the allocated memory space.

But the CPU optimization will reorder the execution instructions, that is to say, the execution order of the above execution process may be 1-2-3 or 1-3-2. If the execution order is 1-3-2, when thread A executes to step 3, it switches to thread B. at this time, thread B judges that the instance object has pointed to the corresponding memory space, and it will return directly when it is not null. At this time, because there is no execution step 2, so we get an uninitialized object, which leads to the birth of the problem. The execution time node is shown in the following table:

In order to solve this problem, we can use the keyword volatile to decorate the instance object, so as to prevent the CPU instructions from being rearranged, so as to run the lazy mode perfectly. The implementation code is as follows:

public class Singleton {
    // Declare private objects
    private volatile static Singleton instance;
    // Get instance (singleton object)
    public static Singleton getInstance() {
        // First judgment
        if (instance == null) {
            synchronized (Singleton.class) {
                // Second judgment
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

Single example other implementation methods

In addition to the above six ways to implement singleton mode, you can also use static inner class and enumeration class to implement singleton. The implementation code of static internal class is as follows:

public class Singleton {
    // Static inner class
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }
    // Get instance (singleton object)
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

From the above code, we can see that the static internal class and the hungry man method have the same magic. They all adopt the mechanism of class loading to ensure that only one thread executes when initializing an instance, thus ensuring the safe operation under multithreading. The JVM will create a lock in the class initialization stage (that is, the class loading stage). The lock can ensure that multiple threads perform the class initialization synchronously. Therefore, in the multi-threaded environment, the class loading mechanism is still thread safe.

But there are also subtle differences between static internal classes and starving methods. The starving method loads when the program starts, which may cause waste of resources; while static internal classes only load internal classes when the getInstance() method is called to complete the initialization of the instance, so it will not cause waste of resources. It can be seen that this method is also the recommended single instance implementation method.

Another implementation of singleton is enumeration, which is also recommended by the author of Effective Java. Because enumeration is not only thread safe, but also can only be loaded once. No matter serialization, deserialization, reflection or cloning, no new objects will be created. Its implementation code is as follows:

public class Singleton {
    // Enumeration types are thread safe and will only be loaded once
    private enum SingletonEnum {
        INSTANCE;
        // Declare singleton object
        private final Singleton instance;
        // instantiation 
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // Get instance (singleton object)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // Class method
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

The execution results of the above procedures are as follows:

Hi,Java.

Summary

In this lesson, we talked about eight ways to implement singleton, including starved mode, which is thread safe but may cause waste of system resources, and five ways to implement lazy mode and lazy mode variants. Among them, there are two kinds of lazy mode of double detection lock, the last two kinds of thread safe static inner class and enumeration class, and the latter two kinds of singleton mode are recommended.

Topics: Java Programming jvm