Everyone is familiar with the singleton model, and they all know why it is lazy, hungry, etc. But do you have a thorough understanding of the singleton pattern? Today I will take you to see the singletons in my eyes, which may be different from your understanding.
Here is a simple small example:
//Simple lazy public class Singleton { //Singleton instance variable private static Singleton instance = null; //Private construction method to ensure that external classes cannot be instantiated through constructor private Singleton() {} //Get singleton instance public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } System.out.println("I am a simple lazy singleton!"); return instance; } }It is easy to see that the above code is unsafe in the case of multi-threading. When two threads enter if (instance == null), both threads judge that the instance is empty, and then two instances will be obtained. This is not the singleton we want.
Next, we use locking to achieve mutual exclusion to ensure the implementation of singletons.
//Synchronous method lazy public class Singleton { //Singleton instance variable private static Singleton instance = null; //Private construction method to ensure that external classes cannot be instantiated through constructor private Singleton() {} //Get singleton instance public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } System.out.println("I am a synchronous method lazy singleton!"); return instance; } }Adding synchronized does ensure thread safety, but is this the best way? Obviously it is not, because in this way, every time we call getInstance() method, we will be locked, and we only need to lock it when we call getInstance() the first time. This obviously affects the performance of our program. We continue to find better ways.
After analysis, it was found that only by ensuring that instance = new Singleton() is thread mutual exclusion, thread safety can be ensured, so the following version is available:
//Double lock lazy public class Singleton { //Singleton instance variable private static Singleton instance = null; //Private construction method to ensure that external classes cannot be instantiated through constructor private Singleton() {} //Get singleton instance public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } System.out.println("I am a double lock lazy singleton!"); return instance; } } This time it seems that not only solves the thread safety problem, but does not cause locking to be added every time you call getInstance(), resulting in performance degradation. It looks like a perfect solution, is it actually that way?
Unfortunately, the fact is not as perfect as we thought. There is a mechanism called "out-of-order writes" in the java platform memory model. It is this mechanism that causes the failure of the double check locking method. The key to this problem lies in line 5 on the above code: instance = new Singleton(); This line actually does two things: 1. Call the constructor and create an instance. 2. Assign this instance to the instance variable instance. But the problem is that these two steps of jvm do not guarantee the order. That is to say. It may be that the instance has been set to non-empty before calling the constructor. Let’s analyze it together:
Suppose there are two threads A and B
1. Thread A enters the getInstance() method.
2. Because instance is empty at this time, thread A enters the synchronized block.
3. Thread A executes instance = new Singleton(); Sets the instance variable instance to non-empty. (Note that it is before calling the constructor.)
4. Thread A exits and thread B enters.
5. Thread B checks whether the instance is empty, and it is not empty at this time (in the third step, it is set to non-empty by thread A). Thread B returns a reference to instance. (The problem arises. At this time, the reference to instance is not a Singleton instance because the constructor is not called.)
6. Thread B exits and thread A enters.
7. Thread A continues to call the constructor method, completes the initialization of instance, and returns.
Isn't there a good way? There must be a good way, let’s continue to explore!
//Solve the problem of unordered writing lazy public class Singleton { //Singleton instance variable private static Singleton instance = null; //Private construction method to ensure that external classes cannot be instantiated through constructor private Singleton() {} //Get singleton instance public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { //1 Singleton temp = instance; //2 if (temp == null) { synchronized (Singleton.class) { //3 temp = new Singleton(); //4 } instance = temp; //5 } } } System.out.println("I am solving the unordered writing of lazy singletons!"); return instance; } } 1. Thread A enters the getInstance() method.
2. Because instance is empty, thread A enters the first synchronized block in position //1.
3. Thread A executes the code at position //2 and assigns instance to the local variable temp. instance is empty, so temp is also empty.
4. Because temp is empty, thread A enters the second synchronized block in position //3. (I thought that this lock was a bit redundant)
5. Thread A executes the code at position //4, setting temp to non-empty, but the constructor has not been called yet! ("Unorder writing" problem)
6. If thread A blocks, thread B enters the getInstance() method.
7. Because instance is empty, thread B tries to enter the first synchronized block. But because thread A is already inside. So it is impossible to enter. Thread B blocks.
8. Thread A is activated and continue to execute the code in position //4. Call the constructor. Generate an instance.
9. Assign the instance reference of temp to instance. Exit two synchronized blocks. Returns the instance.
10. Thread B is activated and enters the first synchronized block.
11. Thread B executes the code at position //2 and assigns the instance instance to the temp local variable.
12. Thread B determines that the local variable temp is not empty, so skips the if block. Returns the instance instance.
So far, we have solved the above problem, but we suddenly found that in order to solve the thread safety problem, it feels like there is a lot of yarn wrapped around on the body... It's messy, so we need to streamline it:
//Hungry public class Singleton { //Singleton variable, static, is initialized once when the class is loaded to ensure thread safety private static Singleton instance = new Singleton(); //Private construction method to ensure that external classes cannot be instantiated through the constructor. private Singleton() {} //Get the singleton object instance public static Singleton getInstance() { System.out.println("I am a hungry singleton!"); return instance; } }When I saw the code above, I instantly felt that the world was quiet. However, this method adopts the Hungry Man-style method, which is to pre-declare Singleton objects. One disadvantage of this is: if the singleton of the construction is large and it is not used after the construction is completed, it will lead to waste of resources.
Is there a perfect way? Continue to watch:
//Inner class implements lazy public class Singleton { private static class SingletonHolder{ //Singleton variable private static Singleton instance = new Singleton(); } //Private construction method to ensure that external classes cannot be instantiated through the constructor. private Singleton() { } //Get singleton object instance public static Singleton getInstance() { System.out.println("I am an internal class singleton!"); return SingletonHolder.instance; } }Lazy (avoid resource waste above), thread-safe, and simple code. Because the Java mechanism stipulates that the internal class SingletonHolder will only be loaded when the getInstance() method is called for the first time (implementing lazy), and its loading process is thread-safe (implementing thread-safe). Instance is instantiated when the internal class is loaded.
Let’s briefly talk about the unordered writing mentioned above. This is the characteristic of jvm. For example, declaring two variables, String a; String b; jvm may load a first or b. Similarly, instance = new Singleton(); may set instance to non-empty before calling Singleton's constructor. This is a question for many people, saying that an object of Singleton has not been instantiated, so how did instance become non-empty? What is its value now? If you want to understand this problem, you must understand how the sentence instance = new Singleton(); is executed. Here is a pseudo-code to explain it to you:
mem = allocate(); //Allocate memory for Singleton objects. instance = mem; //Note that the instance is non-empty now, but has not been initialized yet. ctorSingleton(instance); //Call the constructor of Singleton and pass instance.
It can be seen that when a thread executes instance = mem;, instance is non-empty. If another thread enters the program and judges instance as non-empty, it will jump to return instance; and at this time, Singleton's constructor has not called instance, and the current value is the memory object returned by allocate();. So the second thread gets not a singleton object, but a memory object.
The above is my little thoughts and understanding of the singleton model. I warmly welcome all the great gods to come and guide and criticize.