Singleton Pattern and Its Controversies

The reason why this article suddenly started, because this week’s work, the use of singleton mode to achieve a function, in the code review, was pointed out as far as possible not to use singleton mode, gave me a few articles to see, harvest some content, so systematically summarize.

Singleton mode

First, let’s talk about what the singleton pattern is and how it is implemented.

What is Singleton Mode?

Here is the Lingo definition:

Singleton pattern, also known as monolithic pattern, is a commonly used software Design pattern and belongs to one of the creation patterns. When applying this pattern, the class of a singleton object must ensure that only one instance exists. Many times the entire system only needs to have one global object, which is conducive to coordinating the behavior of the entire system. For example, in a server program, the configuration information of the server is stored in a file, which is uniformly read by a singleton object, and then other objects in the service process obtain the configuration information through this singleton object. This approach simplifies configuration management in complex environments.

The idea of implementing the singleton pattern is: a class can return a reference to an object (always the same) and a method to obtain the instance (must be a static method, usually using the name getInstance); when we call this method, if the class The reference held by the class is not null, the reference is returned, and if the reference held by the class is null, an instance of the class is created and the reference of the instance is assigned to the reference held by the class; at the same time, we also define the constructor function of the class as a private method, so that code elsewhere cannot instantiate the object of the class by calling the constructor function of the class, only through the Class provides a static method to obtain a unique instance of the class.

Singleton mode must be used carefully in multi-threaded applications. If two threads call the create method at the same time when the unique instance has not yet been created, then they do not detect the existence of the unique instance at the same time, so that two instances are constructed at the same time, which violates the singleton mode. The principle of instance uniqueness. The solution to this problem is to provide a mutual exclusion for variables indicating whether the class has been instantiated (although this will reduce efficiency).

Several ways to implement the singleton pattern

There are many ways to implement the singleton mode, as long as only one instance is guaranteed, which is also different in single-threaded and multi-threaded situations. We will briefly introduce several

Hungry man style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonEH {
/**
* Whether Lazy is initialized: No
Whether it is multi-thread safe: Yes
* Implementation difficulty: easy
This method is more commonly used, but it is easy to generate garbage objects.
Advantages: Without locking, execution efficiency will be improved.
Disadvantages: Class is initialized when loaded, wasting memory.
It avoids multi-threaded synchronization issues based on the classloder mechanism.
However, instance is instantiated when the class is loaded, although there are many reasons for class loading.
In singleton mode, most calls are made to the getInstance method.
However, it is not certain that there are other ways (or other static methods) to cause class loading.
At this time, initializing instance obviously does not achieve the effect of lazy loading.
*/
private static SingletonEH instance = new SingletonEH();
private SingletonEH (){}
public static SingletonEH getInstance() {
System.out.println("instance:"+instance);
System.out.println ("Load Hungry Han Style....");
return instance;
}
}

Hungry Han style is inherently thread-safe and can be used directly for multi-threading without problems

Lazy man style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
///Implementation of singleton pattern
/// </summary>
public class Singleton
{
//Define a Static Variable to hold an instance of the class
private static Singleton uniqueInstance;

Define a private constructor function so that the outside world cannot create an instance of the class
private Singleton()
{
}

/// <summary>
///Define public methods to provide a global access point, and you can also define public properties to provide global access points
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
//Create if the instance of the class does not exist, otherwise return directly
if (uniqueInstance null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}

The implementation of the singleton pattern above is indeed perfect under single thread, but in the case of multi-threading, you will get multiple Singleton instances, because when two threads run the GetInstance method at the same time, the two threads judge (uniqueInstance null)这个条件时都返回真,此时两个线程就都会创建Singleton的实例,这样就违背了我们单例模式初衷了,既然上面的实现会运行多个线程执行,那我们对于多线程的解决方案自然就是使GetInstance方法在同一时间只运行一个线程运行就好了,也就是我们线程同步的问题了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// <summary>
///Implementation of singleton pattern
/// </summary>
public class Singleton
{
//Define a Static Variable to hold an instance of the class
private static Singleton uniqueInstance;

//Define an identifier to ensure thread synchronization
private static readonly object locker = new object();

Define a private constructor function so that the outside world cannot create an instance of the class
private Singleton()
{
}

/// <summary>
///Define public methods to provide a global access point, and you can also define public properties to provide global access points
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
//When the first thread runs here, the locker object will be "locked" at this time,
//When the second thread runs this method, it first detects that the locker object is in "locked" state, and the thread will suspend waiting for the first thread to unlock
//The object is "unlocked" after the lock statement has run (that is, after the thread has run)
lock (locker)
{
//Create if the instance of the class does not exist, otherwise return directly
if (uniqueInstance null)
{
uniqueInstance = new Singleton();
}
}

return uniqueInstance;
}
}

The above solution can indeed solve the problem of multi-threading, but the above code will lock the thread auxiliary object locker for each thread and then determine whether the instance exists. It is completely unnecessary for this operation, because when the first thread creates After an instance of this class, the later threads only need to directly judge (uniqueInstancenull)为假,此时完全没必要对线程辅助对象加锁之后再去判断,所以上面的实现方式增加了额外的开销,损失了性能,为了改进上面实现方式的缺陷,我们只需要在lock语句前面加一句(uniqueInstanceThe judgment of null can avoid the extra overhead added by locking. We call this implementation “double locking”. Let’s take a look at the implementation code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
///Implementation of singleton pattern
/// </summary>
public class Singleton
{
//Define a Static Variable to hold an instance of the class
private static Singleton uniqueInstance;

//Define an identifier to ensure thread synchronization
private static readonly object locker = new object();

Define a private constructor function so that the outside world cannot create an instance of the class
private Singleton()
{
}

/// <summary>
///Define public methods to provide a global access point, and you can also define public properties to provide global access points
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
//When the first thread runs here, the locker object will be "locked" at this time,
//When the second thread runs this method, it first detects that the locker object is in "locked" state, and the thread will suspend waiting for the first thread to unlock
//The object is "unlocked" after the lock statement has run (that is, after the thread has run)
Double locking only requires one sentence of judgment
if (uniqueInstance null)
{
lock (locker)
{
//Create if the instance of the class does not exist, otherwise return directly
if (uniqueInstance null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

Why try not to use singleton mode

First of all, programs that use global state are difficult to test. One of the hallmarks of testability is the loose coupling of classes, allowing you to isolate a single class and test it completely. When a class uses a singleton (I’m talking about a classic singleton, a singularity that enforces itself through a static getInstance () method), singleton users and singletons are inevitably coupled together. If you don’t test the singleton pattern, it’s impossible to test the singleton’s user (because that user is bound to call the singleton). In many cases, this is a deal breaker and can prevent developers from testing a class, especially if the singleton represents a resource that should not be updated by testing (e.g. an important database). ** The ideal solution here is to pass the singleton as an argument in the user’s constructor function **, allowing testers to easily simulate the singleton for testing. The singleton pattern then does not have to enforce its own singularity; this can be handled by the client or factory class, which can generate an actual or test version, eliminating the global state completely. In fact, making an object responsible for its own singular and normal tasks should be considered a violation of the single responsibility principle of OO design.

Secondly, programs that rely on global state hide their dependencies. One of the unique features of the singleton pattern is that it can be accessed anywhere through its globally available static method (i.e. getInstance ()), allowing programmers to use it inside the method without having to explicitly pass it through parameters. Although this seems easier for programmers, ** relying on this static instance means that the signature of the method no longer shows their dependencies **, because this method can get a singleton “out of thin air”. This means that users need to understand the inner workings of the code to use it correctly, which makes it more difficult to use and test.

Connecting these two issues shows another problem with the singleton pattern. In today’s world of test-driven and agile development, it is more important than ever to have small tests that cover most of the code. An important thing about these tests is that they must be able to run in any order (without interdependencies), which can become an issue for singleton usage. Since the dependencies of some given methods when they depend on singletons (taken from static getters) are not clear, testers may not know to write two tests that actually depend on each other by modifying shared resources (singletons). This can result in flaky tests that pass when run in one order but fail when run in another, which are of little use.

Actually, ** there is nothing wrong with having only one object instance. But utilizing the “singleton pattern” to achieve this is never the best solution **. At the end of this article, I will tell you why and how to do it better.

Let’s use an example to illustrate what design principles the singleton pattern violates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InterestingThingsDoer {
private static InterestingThingsDoer instance = new InterestingThingsDoer();

//possible some instance state

private InterestingThingsDoer() {}

public static InterestingThingsDoer getInstance() {
return instance;
}

public ResultType doSomethingInteresting() {
//something interesting
}
}

Then we call this singleton in a function of another class

1
2
3
4
5
public void evenMoreInteresting() {
InterestingThingsDoer doer = InterestingThingsDoer.getInstance();
ResultType result = doer.doSomethingInteresting();
//do something even more interesting...
}

Difficult to test

A singleton couples all callers into the concrete implementation of a singleton because the callee demands a singleton object. Your classes become harder to test: you can’t test them individually, you can’t run code in Singleton.

In the example above, the method “evenMoreInteresting” cannot be tested without running some code from “InterestingThingsDoer”.

So what’s wrong with this?

Imagine the following scenario:

1
2
3
4
5
6
public void evenMoreInteresting() {
InterestingThingsDoer doer = InterestingThingsDoer.getInstance();
ResultType result = doer.doSomethingInteresting();
ResultTypeDao dao = ResultTypeDao.getInstance();
dao.save(result);
}

Now, you need a database to test “evenMoreInteresting”. And, if “evenMoreInteresting” is defined in a singleton again (for example, because it is part of a stateless service, and everyone knows that you only need one instance of a stateless service), then you will need a database to test every method that calls “evenMoreInteresting”. And so on. A large part of your testing will be integration testing, and those are evil

Violation of inversion of control

When you use a singleton object in a class, you violate “inversion of control”: your class does not inject collaborators (singleton objects).

In the above code, evenMoreInteresting () has full control over the flow of control, because it takes the concrete implementations of all necessary collaborators and decides what to do with them.

To achieve inversion of control, you must ensure that the class is not responsible for finding/creating their collaborators (among other things). You can use a technique called “dependency injection” - preferably “construct function injection”.

However, if you inject too much, it may cause the constructor function signature to become too large!

If you change some existing code for dependency injection, especially constructor function injection, you will notice that some of your constructor functions have many, many parameters. Take the opportunity to refactor: this class is obviously doing too much!

Violation of the Open Closed Principle

The singleton itself violates the “open/closed principle”: if you want to extend or change the behavior of the singleton, you must change the class. In other words, you cannot change the behavior of “InterestingThingsDoer” from above without opening the file and editing the code.

Changing code is more risky than adding new code. When you change existing code, it is always possible to introduce unwanted side effects (i.e. “defects” or “bugs”) in completely different areas of the code that depend on changing the code.

When you can extend the functionality of existing code by adding new code, the possibility of introducing side effects in “irrelevant” parts of the code is greatly reduced (but still may be greater than zero, depending on your architecture and design).

Therefore, classes/modules/functions that violate the open/closed principle increase the likelihood of introducing defects later when they need to be changed.

Violation of the Single Responsibility Principle

The singleton model itself violates the “single responsibility principle”: in addition to their primary responsibility, they are also responsible for fulfilling their own lifecycle.

Classes that use singletons also violate the “single responsibility principle”: in addition to their primary responsibility, they are also responsible for determining the specific class (at least one) of their collaborators.

The responsibility of each class is also a reason to change. Whenever the “InterestingThingsDoer” needs to be changed, whenever its lifecycle changes, you must change it. When the lifecycle of the “InterestingThingsDoer” changes, you must change all callers because they also have multiple responsibilities.

And, as mentioned earlier, changing the code is inherently risky: if you change the code because some responsibilities of the class have changed, there is also a certain risk that you may break the code that handles other responsibilities of the same class.

In addition, if the class has multiple responsibilities, all of them must be tested. You may not be able to test them individually. This means you have to write more complex/difficult to understand tests.

Violation of the Dependence Inversion Principle

"The Dependency Inversion Principle states that a system’s high-level strategy should not depend on low-level details. Furthermore, abstractions should not depend on details.

Singletons usually implement some low-level details. When you use them from business logic (the system’s high-level policy), you violate the “Dependency Inversion Principle”.

This dependency inversion principle ensures that you can change the low-level details of the system without having to change the high-level “business logic”.

Imagine a vending machine: you choose a product, pay the price, and get the product. When the implementation of this high-level workflow has any dependencies on low-level details, such as how payment works, when you add some new payment method (such as NFC payment), you may have to change the code that handles this high-level workflow. You don’t want this because changing the code is dangerous.

Violation of high cohesion, low coupling

Often, multiple classes depend on the concrete implementation of a singleton. This increases coupling in the system: when you want to change the behavior of a singleton, you may have to check and change all callers. There is always a common coupling between all classes that use singletons: they share a global variable - the singleton!

Sometimes, moving code to singletons (for example, to minimize code duplication) also reduces cohesion: you move things to different classes/packages that should conceptually be part of the original class/package.

In a tightly coupled system, changes ripple through the system: you want to change a little bit of functionality, but to do that you have to modify 20 classes and 50 tests.

In a system with low cohesion, it can be difficult to even find where to make changes: it could be any of 10 different modules. It could also be all of them. Sometimes it’s even hard to know if you’ve found all the places you need to make changes to fix a small flaw.

How to use the Singleton pattern better

As mentioned earlier, there is no problem with only one object instance, but we can completely not use getInstance

Here are two better practices:

If you really only want one instance of a class, implement an inversion of control for your dependencies. If you are already using a dependency injection container (such as Guice or Spring), just make sure that all singleton objects are managed by this container. All of these containers have a way to declare objects as singletons.

If you don’t want to use one of these containers, just create all the object instances that need singletons in one place and inject them into other objects through their constructor function. You may only need one instance for the entire lifecycle of the application, but you don’t need the singleton pattern. Just construct one and inject it into the class as needed.

Stateful Singletons and Stateless Singletons

There are basically two types of singletons:

  • Stateless Singleton: This class doesn’t need multiple instances because it has no state. But you can have many instances of this: it doesn’t matter. Because they have no state

Stateful Singleton: You must have an instance of this class because it represents some globally shared state.

The first class is not a problem. Just use one of the “sensible” methods to create a singleton from above, don’t use the “singleton pattern”. Even if you construct more than one class, this is not a problem: the class is stateless anyway, and you just end up consuming more resources. For most applications (but not all), this overhead may be negligible.

The second category can even be a problem when you use a “sensible” approach to ensuring that there is only one object: shared, mutable state makes it easy to introduce defects when changing content and makes it difficult to scale the application.

Reference article:
什么是单例模式
单例懒汉式和饿汉式的区别
单例实现的几种方法
为什么单例模式令人困扰
为什么要尽量避免单例模式