Novices learn the singleton pattern of java design pattern

  • 2020-11-20 06:05:44
  • OfStack

The singleton pattern is not new, and we all know what it's like to be lazy or hungry. But do you understand singleton patterns well enough? Today I'm going to take you to look at what I think of as singletons, which may be different from what you might think.

Here's a simple little example:


// Simple lazy style  
public class Singleton { 
   
  // Singleton instance variables  
  private static Singleton instance = null; 
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor  
  private Singleton() {} 
   
  // Gets a singleton object instance  
  public static Singleton getInstance() { 
     
    if (instance == null) {  
      instance = new Singleton();  
    } 
     
    System.out.println(" I'm a simple lazy singleton! "); 
    return instance; 
  } 
} 

It is easy to see that the above code is not safe in the case of multiple threads. When two threads enter if (instance == null), both threads decide that instance is null, and then you get two instances. This is not the singleton we want.

Next we implement the mutex by locking to ensure the singleton.


// Synchronized lazy Style  
public class Singleton { 
   
  // Singleton instance variables  
  private static Singleton instance = null; 
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor  
  private Singleton() {} 
   
  // Gets a singleton object instance  
  public static synchronized Singleton getInstance() { 
     
    if (instance == null) {  
      instance = new Singleton();  
    } 
     
    System.out.println(" I'm a synchronized lazy singleton! "); 
    return instance; 
  } 
} 

Adding synchronized does guarantee thread safety, but is that the best approach? Obviously it's not, because the 1 will be locked every time the getInstance() method is called, and we only need to lock it the first time we call getInstance(). This obviously affects the performance of our program. We continue to look for better ways.

After analysis, it was found that only ensuring instance = new Singleton() is thread exclusive can guarantee thread safety, so the following version was produced:


// Double locked Slacker  
public class Singleton { 
   
  // Singleton instance variables  
  private static Singleton instance = null; 
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor  
  private Singleton() {} 
   
  // Gets a singleton object instance  
  public static Singleton getInstance() { 
    if (instance == null) {  
      synchronized (Singleton.class) { 
        if (instance == null) {  
          instance = new Singleton();  
        } 
      } 
    } 
    System.out.println(" I'm a double lock slacker singleton! "); 
    return instance; 
  } 
} 

This time seems to address the thread-safety issue without locking every time getInstance() is called. Looks like a perfect solution, but is it?

Unfortunately, it's not as perfect as we thought. There is a mechanism in the java platform memory model called "unordered write" (out-ES35en-ES36en writes). It is this mechanism that causes the double-checked locking method to fail. The crux of the problem lies in line 5 of the above code: instance = new Singleton(); This line does two things: 1. It calls the constructor and creates an instance. 2. Assign the instance to the instance variable instance. The problem is that these two steps jvm do not guarantee order. That is to say. It is possible that instance has been set to non-null before the constructor is called. Let's start with 1 and analyze 1:

Suppose there are two threads, A and B

1. Thread A enters the getInstance() method.
2. Because instance is empty at this time, the thread A enters synchronized block.
3. Thread A = new Singleton(); Set the instance variable instance to non-null. (Note that this is before the constructor is called.)
4. Thread A exits and thread B enters.
5. Thread B checks to see if instance is empty, and it is not empty at this time (in step 3, it was set to non-empty by thread A). Thread B returns a reference to instance. (The problem arises when the reference to instance is not an instance of Singleton because the constructor is not called.)
6. Thread B exits and thread A enters.
7. Thread A continues to call the constructor, completes the initialization of instance, and then returns.

Isn't there a good way? There must be a good way, we continue to explore!


// Slackers solve unordered writing problems  
public class Singleton { 
   
  // Singleton instance variables  
  private static Singleton instance = null; 
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor  
  private Singleton() {} 
   
  // Gets a singleton object 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 disorder write lazy singleton! "); 
    return instance; 
  }   
} 

1. Thread A enters getInstance() method.
2. Because instance is empty, thread A enters the first synchronized block at position //1.
3. Thread A executes the code of location //2 and assigns instance to the local variable temp. instance is empty, so is temp.
4. Because temp is empty, thread A enters the second synchronized block of position //3. (Later thinking the lock was a bit redundant)
5, thread A execution location //4 code, temp set to non-null, but has not called the constructor! (" Unordered writing "problem)
6. If thread A blocks, thread B enters the getInstance() method.
7. Because instance is empty, thread B attempts to enter the first synchronized block. But since thread A is already inside. So I can't get in. Thread B blocks.
8, thread A activated, continue to execute the location //4 code. Call the constructor. Generate instances.
Assign the instance reference of temp to instance. Exit both synchronized blocks. Returns an instance.
10. Thread B is activated and enters the first synchronized block.
11. Thread B executes the code at location //2 and assigns an instance of instance to the local variable temp.
12. Thread B determines that local variable temp is not empty, so it skips the BLOCK of if. Returns an instance of instance.

So far, we have solved the above problem, but suddenly we find that to solve the thread safety problem, it feels like we have a lot of wool wrapped around our body... It's a mess, so let's cut it down by 1:


// The hungry type  
public class Singleton { 
   
  // Singleton variable  ,static Is initialized when the class loads 1 Second, ensure thread safety   
  private static Singleton instance = new Singleton();   
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor.     
  private Singleton() {} 
   
  // Gets a singleton object instance     
  public static Singleton getInstance() { 
    System.out.println(" I'm the Hungry Man singleton! "); 
    return instance; 
  } 
} 

See the above code, the world suddenly feel quiet. However, this approach takes the Hunger-Man approach of pre-declaring Singleton objects, which brings one disadvantage: if the constructed singleton is large and is not used after construction, resources will be wasted.

Is there a perfect way? Continue to look at:


// The inner class implements the lazy style  
public class Singleton { 
   
  private static class SingletonHolder{ 
    // Singleton variable   
    private static Singleton instance = new Singleton(); 
  } 
   
  // A privatized constructor that guarantees that an external class cannot be instantiated by a constructor.  
  private Singleton() { 
     
  } 
   
  // Gets a singleton object instance  
  public static Singleton getInstance() { 
    System.out.println(" I'm an inner class singleton! "); 
    return SingletonHolder.instance; 
  } 
} 

Slackers (avoid the waste of resources above), thread safety, and simple code. Because the java mechanism states that the inner class SingletonHolder is loaded only the first time the getInstance() method is called (lazy is implemented), and the loading process is thread-safe (thread-safe implemented). instance is instantiated once when the inner class is loaded.

To put it simply, the unordered writing mentioned above is a feature of jvm, such as declaring two variables, String a; String b; jvm may load a first or b first. Similarly, instance = new Singleton(); It is possible to make instance non-null before calling the constructor for Singleton. This is one of the questions that many people will ask, saying that Singleton hasn't been instantiated yet, so how does instance become non-empty? What is its value now? To understand the problem, understand that instance = new Singleton(); How this sentence is implemented, the following use 1 pseudocode to explain to you 1:


mem = allocate();       // for Singleton Object allocates memory.  
instance = mem;        // Pay attention to now instance Non-empty, but not yet initialized.  
 
ctorSingleton(instance);  // call Singleton Constructor, transfer instance. 

Thus it can be seen that when a thread executes to instance = mem; When, instance is already non-null. If another thread enters the program at this time to determine that instance is non-null, then it directly jumps to return instance. At this point, Singleton's constructor has not yet called instance, and the value is allocate(); The returned memory object. So the second thread gets not one object for Singleton, but one memory object.

The above is my one little thought and understanding of the singleton pattern. I warmly welcome all the great gods to guide and criticize it.


Related articles: