Design mode is a solution to design problems encountered in the process of software development. Most design patterns are intended to solve the problem of code extensibility. By learning design patterns, we need to understand the code pain points solved and the application scenarios of design patterns, and it is not advisable to copy them mechanically and overuse them.

With the development of programming languages, some (e.g The Singleton pattern is out of date, and even considered antipatterns by some, and built into programming languages (e.g Iterator pattern Iterator), popular frameworks, and new patterns (e.g. MVC, Service Locator pattern Service Locator, Delegation pattern).

GoF has 23 design patterns, which can be divided into creation, structure and behavior.

This article covers the creative design pattern, providing a mechanism for creating objects and increasing the flexibility and reusability of existing code. Among them:

  • Commonly used are:Singleton pattern, Factory pattern (factory method and abstract factory), Builder pattern;
  • Less commonly used are:The prototype pattern;

Next, let’s look at the Singleton Design Pattern.

What is the singleton pattern?

There aren’t many common design patterns in the classic GoF 23, but pick up any wild programmer on the road and he’ll know what a singleton is.

Singleton Design patterns are simple to understand. If a class is allowed to create only one object (or instance), that class is a singleton class. This design pattern is called the singleton design pattern, or singleton for short.

Why use the singleton pattern?

From a business perspective, if only one copy of some data should be stored in the application, it is better to design it as a singleton. For example, configuration classes.

Another example is that in our everyday business, entity classes have pipelines (unique ids). We need an ID generator that generates unique ids, and there should only be one ID generator. Why? Because there are multiple ID generators, it is highly likely that duplicate ids will result, which is not what we expect.

public class IDGenerator {
    // AtomicLong is a Java concurrency library that provides thread-safe long addition and subtraction operations
    private AtomicLong id = new AtomicLong();
    
    private static final IDGenerator INSTANCE = new IDGenerator();

    private IDGenerator(a) {}
    
    public static IDGenerator getInstance(a) {
        return INSTANCE;
    }
    
    public long allocId(a) {
        returnid.incrementAndGet(); }}...// Assign a new ID
IDGenerator.getInstance().allocId();
Copy the code

In the example above, the id generator that generates unique ids is implemented, but the implementation is not elegant and there are some glitches. What are the glitches? There are answers below.

How to use the singleton pattern?

In the singleton pattern, only one object is allowed to be created from a class.

The singleton pattern concept is simple. How do we implement it? Let’s start with the following code snippet:

// Error in multithreaded environment
public class Supporter {

    private static Supporter supporter;
    
    private Supporter(a) {}
    
    public static Supporter getInstance(a) {
        if (supporter == null) {// A
            supporter = new Supporter();
        }
        returnsupporter; }}Copy the code

This code is fine when used in a single thread. But in a multithreaded environment, this code becomes problematic. Method getInstance() is called in parallel in A multi-threaded environment, there is A chance that multiple threads execute to line A, all true, will cause object to be created many times, which violates the singleton mode convention.

So, in order to solve the problem, we try to introduce locks to ensure that multiple threads are serialized, which is lazy style:

/ / LanHanShi
// Correct, but inefficient
public class Supporter {

    private static Supporter supporter;
    
    private Supporter(a) {}
    
    public static synchronized Supporter getInstance(a) {
        if (supporter == null) {
            supporter = new Supporter();
        }
        returnsupporter; }}Copy the code

Synchronized to lock the method, ensuring that serial execution in multi-thread, catalyst will only be created once. However, it also introduces efficiency issues, because the concurrency is only 1, which can degrade code performance by a factor of 100 or more in extreme cases. After creating the Catalyst instance, it doesn’t seem necessary to get the lock every time you enter a method.

Therefore, someone proposed the practice of “double-checked Locking” :

/ / error
public class Supporter {

    private static Supporter supporter;
    
    private Supporter(a) {}
    
    public static Supporter getInstance(a) {
        
        if (supporter == null) {// Check that the instance has been created
        
            synchronized(Supporter.class) {// Class level lock
                if (supporter == null) {// inside the lock to check if the instance has been created
                    supporter = newSupporter(); }}}returnsupporter; }}Copy the code

Intuitively, the above is a more efficient solution, in a multi-threaded environment, does avoid simultaneous locking, however, it does not work here.

This question, raised in the JSR133 JMM, indicates the goal of the specification and also provides a corresponding answer based on the JSR133 goal. The answer mentions this question, which I excerpted:

Paul Jakubik found an example of a use of double-checked locking that did not work correctly. [A slightly cleaned up version of that code is available here](http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckTest.java).

When run on a system using the Symantec JIT, it doesn't work. In particular, the Symantec JIT compiles


singletons[i].reference = new Singleton();

to the following (note that the Symantec JIT using a handle-based object allocation system).

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

Copy the code

The compiler reordered the initialization of the Singleton and the initialization of the Singleton field, which resulted in a partially constructed Singleton instance in the memory address pointed to by the Singleton field. Other threads operate with a partially initialized instance, and the program may crash. Instead of doing too much expansion, the focus here is on design patterns. To learn more, click on the link above for an official explanation.

In JSR133 JMM solution, the solution of “double-checked Locking” is proposed:

// Double-Checked Locking
// Correct. JDK versions below 1.5 (excluding 1.5) are not volatile
public class Supporter {

    private static volatile Supporter supporter;
    
    private Supporter(a) {}
    
    public static Supporter getInstance(a) {
        
        if (supporter == null) {// Check that the instance has been created
        
            synchronized(Supporter.class) {// Class level lock
                if (supporter == null) {// inside the lock to check if the instance has been created
                    supporter = newSupporter(); }}}returnsupporter; }}Copy the code

Of course, other than the practice of “double-checked Locking”, are there any other practices?

Let’s imagine, class load time, static instances have been created and initialized, catalyst instance creation process is thread safe. This is hungry, and the code for this example is as follows:

/ / the hungry
/ / right
public class Supporter {

    private static final Supporter supporter = new Supporter();
    
    private Supporter(a) {}
    
    public static Supporter getInstance(a) {
        returnsupporter; }}Copy the code

Some people think this way is not good, because does not support lazy loading, if the instance objects occupy more resources (e.g. memory) or initialization time consuming (involves takes a relatively long IO operations), initialize the instance in advance is a waste of resources, the best way to be in when used to initialize. However, I do not agree with this view.

Imagine a fail-fast system where we want to initialize instances as soon as the application starts, because instances are consuming too many resources. If the resources are not enough, the program will start with an error, the programmer can immediately repair, rather than waiting for the program to run for a period of time, initialization of the instance caused by sudden insufficient resources (such as OOM), resulting in the process system crash, which will seriously affect the availability.

For example, due to the long initialization time, we should not perform the initialization only when there is demand, which will affect the system performance and cause problems such as lag and timeout. Use the hungry type, in the application starts, completes initialization, can avoid the initialization is triggered when the program is run lead to performance problems.

Let’s take a look at a simpler implementation that uses Java’s static inner classes to achieve lazy loading. Example code is as follows:

// Static inner class
/ / right
public class Supporter {
    
    private Supporter(a) {}
    
    private static class SupporterHolder { 
        private static final Supporter supporter = new Supporter();
    }
    
    public static Supporter getInstance(a) {
        returnSupporterHolder.supporter; }}Copy the code

SupporterHolder is a static internal class, when external class Supporter was loaded, will not create SupporterHolder instance object. SupporterHolder will only be loaded when the getInstance() method is called, when the supporter will be created. Catalyst uniqueness, the creation process of thread safety, are guaranteed by the JVM. Therefore, this implementation method not only ensures thread safety, but also can achieve lazy loading.

Finally, let’s look at the implementation based on the enumeration feature, which ensures thread-safe and unique instance creation through the Java enumeration type feature. Example code is as follows:

/ / the enumeration
/ / right
public enum Supporter {
    
    INSTANCE;
    
    // fields and methods}... Supporter.INSTANCECopy the code

What are the problems with the singleton pattern

Not OOP friendly

The four main features of OOP are encapsulation, abstraction, inheritance and polymorphism. The singleton pattern does not support abstraction, inheritance, or polymorphism well. Why is that?

public class User {
    public void create(...). {...// If the requirements change, the respective module needs to use the id generator of the module
        / / will need the following line, replace long id = XXXIDGenerator. GetInstance () allocId ();
        longid = IDGenerator.getInstance().allocId(); . }}...Copy the code

The example above shows that the singleton pattern violates the interface isolation principle, as well as the abstract nature of OOP. In response to changes in requirements, programmers need to manually modify everything that uses IDGenerator classes, which can be costly to maintain.

In addition to being unfriendly to support abstraction, singletons are equally unfriendly to support inheritance and polymorphism. Singletons can still be inherited, and they can be polymorphic, but when they are implemented, the code looks weird and confusing to the viewer. Therefore, once you choose the singleton pattern, you lose extensibility to future changes in requirements.

Bad code extensibility

A singleton class allows only one instance of an object. Imagine if we had a requirement to create multiple instances in the process, with different modules in different scenarios using different instances, and the changes to the code would be significant.

You may wonder, is there such a need? The IDGenerator mentioned above is also a case in point, here is an example to explain. At the beginning of the system design, there was not much business, so we expected that only one thread pool would be needed in the system. In order to facilitate us to control the consumption of thread resources, we designed the thread pool as a singleton. However, with the increase of business iterations, we found that some businesses took a long time to execute and consumed thread resources, which affected the business that could have been completed quickly and caused queue blocking. To solve this problem, we want to separate the long and short services from each other, so we need to create two thread pools that do not affect each other. However, we designed the thread pool as a singleton, which obviously could not meet this requirement change. Therefore, in some cases, singleton classes can affect the extensibility and flexibility of code.

Poor testability of code

When we write unit tests, we mock them. However, singletons in hard code cannot be mock if there is a dependent resource (e.g. IDGenerator id member variable) that is heavily dependent. Because resource dependencies, which are equivalent to global variables, are shared by all threads in the process, different test cases affect each other’s test results.

Constructors that hold parameters are not supported

The singleton pattern does not support construction parameters. In some cases, the singleton we want to construct needs to support parameter Settings (e.g., thread pool size). So, in this case, what do we need to do?

Scenario 1: Passed in when called (getInstance(params)). This approach has obvious drawbacks. The first call to the passed parameter is valid, and the subsequent passed parameter is invalid, which can easily mislead the caller.

Scheme 2: Parameters are read through configuration. This method is recommended. If you need to modify parameters, you can directly modify them in the configuration. Configurations can be configuration classes, XML, Text, Properties, YAML, and so on.