A deeper understanding of the less simple singleton pattern in Java

  • 2020-05-30 20:18:16
  • OfStack

preface

It is well known that the singleton (Singleton) pattern in Java is a widely used design pattern. The primary purpose of the singleton pattern is to ensure that only one instance of a class exists in the Java program. Some managers and controllers are often designed in singleton mode.

The singleton pattern has many benefits. It avoids the repeated creation of instance objects. It not only reduces the time spent creating objects each time, but also saves memory space. Can avoid logic errors caused by manipulating multiple instances. If an object is likely to span the entire application and act as a global administrative control, the singleton pattern may be an option worth considering.

There are many ways to write singletons, most of which are more or less inadequate. The following sections will introduce each of these approaches.

1. Hungry Chinese mode


public class Singleton{ 
 private static Singleton instance = new Singleton(); 
 private Singleton(){} 
 public static Singleton newInstance(){ 
 return instance; 
 } 
} 

As you can see from the code, the constructor of the class is defined as private, ensuring that no other class can instantiate this class, and then provides a static instance and returns it to the caller. The hunhan pattern is the simplest one to implement. The hunhan pattern creates an instance when the class is loaded, and the instance exists throughout the program cycle. It has the advantage of only creating an instance once when the class is loaded, without multiple threads creating multiple instances, thus avoiding the problem of multi-threading synchronization. The disadvantage is obvious, too, that the singleton is created even if it is not used, and memory is wasted when it is created after the class is loaded.

This implementation is suitable for cases where singletons take up less memory and are used during initialization. However, if the singleton takes up a large amount of memory, or if the singleton is only used in a particular situation, it is not appropriate to use the hungry man mode, which is where lazy mode is needed for lazy loading.

2. Slacker mode


public class Singleton{ 
 private static Singleton instance = null; 
 private Singleton(){} 
 public static Singleton newInstance(){ 
 if(null == instance){ 
  instance = new Singleton(); 
 } 
 return instance; 
 } 
} 

In lazy mode, the singleton is created when needed. If the singleton has been created, the fetch interface is called again and will not recreate the new object, but will return the previously created object. If a singleton is used less often and more resources are needed to create it, then it is necessary to create the singleton on demand. This is a good time to use the lazy pattern. However, the lazy mode here does not take thread safety into consideration. Multiple threads may concurrently call its getInstance() method, resulting in the creation of multiple instances, so locking is required to solve the thread synchronization problem, as shown below.


public class Singleton{ 
 private static Singleton instance = null; 
 private Singleton(){} 
 public static synchronized Singleton newInstance(){ 
 if(null == instance){ 
  instance = new Singleton(); 
 } 
 return instance; 
 } 
} 

3. Double check lock

The locked slacker pattern seems to solve both thread concurrency and lazy loading, but it has performance problems and is still not perfect. The synchronized modified synchronization method is much slower than the 1 general method if it is called multiple times getInstance() , the cumulative performance loss is relatively large. So you have a double check lock, so let's look at the code that implements it.


public class Singleton { 
 private static Singleton instance = null; 
 private Singleton(){} 
 public static Singleton getInstance() { 
 if (instance == null) { 
  synchronized (Singleton.class) { 
  if (instance == null) {//2 
   instance = new Singleton(); 
  } 
  } 
 } 
 return instance; 
 } 
} 

You can see that there is an extra layer of instance empty outside the synchronized code block. Since the singleton object only needs to be created once, if it is called again later getInstance() You just need to return the singleton directly. Therefore, most of the time, calls to getInstance() do not execute into synchronized code blocks, which improves program performance. However, there is one more case to consider, if two threads A, B, and A are executed if (instance == null) Statement, it will think that the singleton object is not created, at this point the thread cut to B also executed the same statement, B also think that the singleton object is not created, and then the two threads in turn execute the synchronized code block, and create a singleton object. To solve this problem, you also need to add to the synchronized code block if (instance == null) Statement, which is code 2 as seen above.

When we see a double-checked lock that implements lazy loading, resolves thread concurrency, and at the same time resolves execution efficiency, is there really nothing missing?
The directive rearrangement optimization in Java is mentioned here. Instruction reordering optimization is to make a program run faster by adjusting the order in which instructions are executed without changing the original semantics. There is no requirement for compiler optimization in JVM, which means that JVM is free to optimize instruction reordering.

The key to this problem is that the order in which Singleton is initialized and the object address is assigned to the instance field is uncertain due to instruction reordering optimization. When a thread creates a singleton object, memory space is allocated to the object and the fields of the object are set to default values before the constructor is called. At this point you can assign the allocated memory address to the instance field, although the object may not have been initialized. If another thread calls getInstance immediately after, it will get the object in the wrong state, and the program will make an error.

That's why double-check locks fail, but JDK1.5 and later added the volatile keyword. One of the semantics of volatile is to prohibit instruction reordering optimization, which ensures that the object is already initialized when the instance variable is assigned, thus avoiding the problem mentioned above.

The code is as follows:


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

4. Static inner class

In addition to the above three ways, there is another way to implement singletons through static inner classes. First take a look at its implementation code:


public class Singleton{ 
 private static class SingletonHolder{ 
 public static Singleton instance = new Singleton(); 
 } 
 private Singleton(){} 
 public static Singleton newInstance(){ 
 return SingletonHolder.instance; 
 } 
} 

This approach also makes use of the class loading mechanism to ensure that only one instance of instance is created. Similar to hunhan mode 1, it also makes use of class loading mechanism, so there is no concurrent problem of multiple threads. Instead, it creates object instances within the inner class. Thus, as long as the inner class is not used in the application, JVM will not load the singleton class and will not create the singleton object, thus achieving lazy lazy loading. In other words, this approach ensures both lazy loading and thread safety.

5, the enumeration

Take a look at the last implementation to be covered in this article: enumeration.


public enum Singleton{ 
 instance; 
 public void whateverMethod(){} 
} 

The four ways of implementing singletons mentioned above all have common disadvantages:

1) extra work is required to implement serialization, otherwise a new instance will be created each time a serialized object is deserialized.

2) you can use reflection to force a call to a private constructor (if you want to avoid this, you can modify the constructor to throw an exception when the second instance is created).

The enumeration class solves both of these problems by providing automatic serialization, in addition to thread safety and protection against reflected invocation constructors, to prevent the creation of new objects when deserialized. Therefore, the method recommended by the authors of Effective Java. In practice, however, this is rarely seen.

conclusion

This paper summarizes 5 ways to implement singletons in Java, the first two of which are not perfect. Double-check locks and static inner classes can solve most problems, and they are also the most commonly used methods in daily work. Enumeration method is a perfect solution to a variety of problems, but this kind of writing more or less let a person feel a little rusty. My personal recommendation is to implement the singleton pattern in ways 3 and 4 without special requirements.

The above is the whole content of this article, I hope the content of this article to your study or work can bring 1 definite help, if you have questions you can leave a message to communicate.


Related articles: