This article will answer some of your questions:

  • Why use singletons?
  • What are the ways to write singletons?
  • What are the problems with singletons?
  • What’s the difference between a singleton and a static class?
  • What are the alternative solutions?
Why use singletons?

A class is a Singleton Design Pattern if it allows only one object (or instance) to be created. This Design Pattern is called a Singleton Design Pattern, or Singleton for short.

Why do we need a singleton design pattern? What problems does it solve? Next, I will explain it through two practical cases:

Actual case 1: Handle resource access conflicts

We implemented a custom Logger class that prints logs to a file. The specific code implementation is as follows:

public class Logger { private FileWriter writer; public Logger() { File file = new File("/Users/wangzheng/log.txt"); writer = new FileWriter(file, true); } public void log(String message) {writer.write(message); Public class UserController {private Logger Logger = new Logger(); public void login(String username, String password) { // ... Omit business logic code... logger.log(username + " logined!" ); } } public class OrderController { private Logger logger = new Logger(); public void create(OrderVo order) { // ... Omit business logic code... logger.log("Created an order: " + order.toString()); }}Copy the code

All logs are written to the same file /Users/wangzheng/log.txt. In UserController and OrderController, we create two Logger objects. In the Servlet multi-threaded environment of the Web container, if two Servlet threads execute login() and create(), respectively, and write logs to the log.txt file at the same time, it is possible to overwrite log information.

Why do they overlay each other? We can think of it this way. In a multi-threaded environment, if two threads increment the same shared variable by 1 at the same time, since the shared variable is a competing resource, the result of the shared variable may not be increment by 2, but only increment by 1. Similarly, the log.txt file is a competing resource, and two threads writing to it at the same time may overwrite each other.

Actual case 2: Represents the global unique class

From a business concept, it is better to design a singleton class if only one copy of some data should be kept in the system.

For example, the configuration information class. In the system, we have only one configuration file, and when the configuration file is loaded into memory, it exists as an object, and of course there is only one.

How do I implement a singleton?

To implement a singleton, we need to pay attention to the following: – Constructors need to be private, so that external instances of new are not created;

  • Consider thread-safe object creation;
  • Consider whether lazy loading is supported;
  • Consider whether lazy loading is supported;
  • Consider whether getInstance() is high performance (locked or not).
1. The hungry

The hungrier implementation is simpler. Instance static instances are created and initialized at class load time, so instance instance creation is thread-safe. However, this implementation does not support lazy loading (creating instances when IdGenerator is actually used), as we can see from the name. The specific code implementation is as follows:

public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static final IdGenerator instance = new IdGenerator(); private IdGenerator() {} public static IdGenerator getInstance() { return instance; } public long getId() { return id.incrementAndGet(); }}Copy the code

Lazy loading is not supported. If an instance consumes a lot of resources (such as memory) or takes a long time to initialize (such as loading various configuration files), it is a waste of resources to initialize the instance in advance. The best way to do this is to initialize it only when needed.

If the instance is resource-intensive, we also want to initialize it at startup, following fail-Fast’s design principles (problems are exposed early). If there are not enough resources, it will trigger an error at startup (such as PermGen Space OOM in Java) and we can fix it immediately. This also prevents the system from crashing and affecting the availability of the system after the program has been running for a period of time due to too many resources taken up by initializing the instance.

2. LanHanShi

Slacker has an advantage over hungrier in that it supports lazy loading. The specific code implementation is as follows:

public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static synchronized IdGenerator getInstance() { if (instance == null) { instance = new IdGenerator(); } return instance; } public long getId() { return id.incrementAndGet(); }}Copy the code

The downside of the lazy style is also obvious, as we put a large lock (synchronzed) on the getInstance() method, resulting in low concurrency. If you quantify it, the concurrency is 1, so it’s a serial operation. This function is called all the time during singleton use. This is acceptable if the singleton class is used occasionally. However, if used frequently, frequent locking, lock release, and low concurrency issues can lead to performance bottlenecks, and this implementation is not desirable.

3. Double detection

Hungry does not support lazy loading, lazy has performance issues and does not support high concurrency. Let’s look at a singleton implementation that supports both lazy loading and high concurrency, that is, the double detection implementation.

In this implementation, once instance is created, even a call to getInstance() does not enter the locking logic again. So, this implementation solves the problem of lazy concurrency. The specific code implementation is as follows:

public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { Synchronized (IdGenerator. Class) {if (instance == null) {instance = new IdGenerator(); synchronized(IdGenerator. } } } return instance; } public long getId() { return id.incrementAndGet(); }}Copy the code

Some say there are problems with this approach. Instruction reordering can result in an IdGenerator object being new and assigned to instance being used by another thread before it can be initialized (executing the code logic in the constructor). To solve this problem, we need to make the instance member variable volatile and forbid instruction reordering. In fact, only very early versions of Java have this problem. The older version of Java we’re using has already solved this problem in the JDK’s internal implementation (the solution is as simple as making the object new and initialization operations atomic, which naturally disables reordering). I won’t go into the details of this as it relates to a particular language, but if you’re interested, you can do your own research.

Static inner classes

Leverage Java’s static inner classes. It’s kind of hungrier, but with lazy loading. How did you do that? Let’s look at the code implementation first.

public class IdGenerator { private AtomicLong id = new AtomicLong(0); private IdGenerator() {} private static class SingletonHolder{ private static final IdGenerator instance = new IdGenerator(); } public static IdGenerator getInstance() { return SingletonHolder.instance; } public long getId() { return id.incrementAndGet(); }}Copy the code

SingletonHolder is a static inner class. When the external class IdGenerator is loaded, the SingletonHolder instance object is not created. The SingletonHolder will only be loaded when the getInstance() method is called, at which point the instance will be created. The uniqueness of instance and the thread-safety of the creation process are guaranteed by the JVM. Therefore, this implementation method not only ensures thread safety, but also can achieve lazy loading.

5. The enumeration

This implementation ensures thread-safe and unique instance creation through the nature of Java enumerated types. The specific code is as follows:

public enum IdGenerator { INSTANCE; private AtomicLong id = new AtomicLong(0); public long getId() { return id.incrementAndGet(); }}Copy the code
What are the problems with singletons?

Singletons are used in projects to represent globally unique classes such as configuration information classes, connection pool classes, and ID generator classes. The singleton mode is simple to write and easy to use. In code, we do not need to create objects, but simply call them with methods like idgenerator.getInstance ().getid (). However, this approach is somewhat similar to hard code and can cause problems. Let’s take a look at some of the problems:

1. Singletons are unfriendly to OOP feature support

The four main features of OOP are encapsulation, abstraction, inheritance and polymorphism. The singleton design pattern does not support abstraction, inheritance, or polymorphism. Why do you say that? Let’s use IdGenerator as an example.

public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}
Copy the code

IdGenerator is used in a way that violates the principle of interface-based rather than implementation-based design, and thus violates the abstract nature of OOP in a broad sense. If one day we want to use different ID generation algorithms for different businesses. For example, order ids and user ids are generated using different ID generators. To cope with this change in requirements, we need to modify everything that uses the IdGenerator class so that the code changes significantly.

public class Order { public void create(...) {/ /... long id = IdGenerator.getInstance().getId(); / / need to one line of code above, replace with one line of code below long id = OrderIdGenerator. GetIntance (). The getId (); / /... } } public class User { public void create(...) {/ /... long id = IdGenerator.getInstance().getId(); / / need to one line of code above, replace with one line of code below long id = UserIdGenerator. GetIntance (). The getId (); }}Copy the code

Once you choose to design a class as a singleton, you give up two powerful object-oriented features, inheritance and polymorphism, and lose extensibility to cope with future changes in requirements.

2. Singleton hides dependencies between classes

Dependencies between classes declared through constructors, parameter passing, and so on can be easily identified by looking at the function definition. However, the singleton class does not need display creation, does not rely on parameter passing, and can be called directly from the function. If the code is complex, this call relationship can be very subtle. As we read the code, we need to look closely at the code implementation of each function to see which singleton classes the class depends on.

3. Singletons are not extensible code friendly

We know that a singleton class can have only one instance of an object. If at some point in the future we need to create two or more instances in our code, we will need to make major changes to our code. You might say, is there a need for that? Since singleton classes are mostly used to represent global classes, why do you need two or more instances?

In fact, such demands are not uncommon. Let’s take database connection pooling as an example.

At the beginning of the system design, we think there should be only one database connection pool in the system, so that we can control the consumption of database connection resources. So, we designed the database connection pool class as a singleton class. But then we found that some SQL statements in the system were running very slowly. These SQL statements occupy database connection resources for a long time, resulting in failure to respond to other SQL requests. To solve this problem, we want to execute slow SQL in isolation from other SQL. To achieve this goal, we can create two database connection pools in the system, one for the slow SQL and the other for the other SQL, so that the slow SQL does not affect the execution of the other SQL.

If we designed the database connection pool as a singleton class, we would not be able to accommodate such a change in requirements, that is, the singleton class would affect the extensibility and flexibility of the code in some cases. Therefore, resource pools such as database connection pools and thread pools should not be designed as singleton classes. In fact, some open source database connection pools and thread pools are not designed as singleton classes.

4. Singletons are not testability friendly to code

The use of singletons affects the testability of your code. If a singleton class relies on a heavy external resource, such as DB, we want to mock it out when writing unit tests. The hardcoded use of singleton classes makes mock substitution impossible.

In addition, if a singleton class holds a member variable (such as the ID member variable in IdGenerator), it is effectively a global variable that is shared by all code. If the global variable is a mutable global variable, that is, its member variables can be modified, then when writing unit tests, we need to pay attention to the problem that different test cases change the value of the same member variable in the singleton class, resulting in mutual influence of test results.

5. Singletons do not support constructors that hold arguments

Singletons do not support constructors that hold arguments. For example, if we create a singleton object with a connection pool, we cannot specify the size of the pool using arguments.

What are the alternative solutions?
Public demofunction() {//... long id = IdGenerator.getInstance().getId(); / /... Public demofunction(IdGenerator IdGenerator) {long id = idGenerator.getid (); } // When demofunction() is called, pass idGenerator idGenerator idGenerator idGenerator = idGenerator.getinsance (); demofunction(idGenerator);Copy the code

Based on the new usage, we can solve the problem of singletons hiding dependencies between classes by passing objects generated by singletons to functions as arguments (and also to class member variables via constructors). However, other problems with singletons, such as not being friendly to OOP features, extensibility, testability, and so on, cannot be solved.

So, if we want to solve these problems completely, we may have to look at the roots and find other ways to implement globally unique classes. In fact, global uniqueness of class objects can be guaranteed in a number of different ways. We can enforce guarantees either through the singleton pattern or through the factory pattern, an IOC container (such as the Spring IOC container).