26 Singleton Pattern How to Create a Single Object to Optimize System Performance

26 Singleton pattern How to create a single object to optimize system performance #

Hello, I’m Liu Chao.

Starting from this lecture, we will explore performance optimization of design patterns. In the book “Design Patterns: Elements of Reusable Object-Oriented Software”, there are descriptions of 23 design patterns, among which the Singleton design pattern is one of the most commonly used. Whether it is in open-source frameworks or in our daily development, the Singleton pattern is almost everywhere.

What is the Singleton pattern? #

The core of the Singleton pattern is to ensure that a class has only one instance and provides a global access point to it.

The pattern has three basic points: first, the class can only have one instance; second, it must create the instance on its own; third, it must provide this instance to the entire system on its own.

Combining these three points, let’s implement a simple Singleton:

// Eager initialization
public final class Singleton {
    private static Singleton instance = new Singleton(); // Create the instance on its own
    private Singleton() {} // Constructor
    public static Singleton getInstance() { // Provide the instance to the system through this function
        return instance;
    }
}

In a system, a class is often used in different places. By using the Singleton pattern, we can avoid creating multiple instances multiple times, thus saving system resources.

Singleton Pattern #

We can see that in the first implementation of the singleton code above, the member variable instance is decorated with static, so this variable will be collected into the class constructor method during class initialization. In a multi-threaded scenario, the JVM ensures that only one thread can execute the class constructor method, while other threads will be blocked and wait.

When the unique class constructor method is completed, other threads will not execute the class constructor method again, but instead execute their own code. In other words, the member variable instance decorated with static, can guarantee that it is only instantiated once in a multi-threaded situation.

This implementation of the singleton pattern allocates a block of memory in the heap at the class loading stage to store the instantiated object, so it is also called the eager initialization pattern.

The advantages of implementing the singleton pattern using the eager initialization pattern are that it can ensure the uniqueness of the instance in a multi-threaded situation, and the getInstance method directly returns the unique instance, which has very high performance.

However, in the case of many class member variables or large variables, this pattern may occupy the heap memory continuously without using the class object. Just imagine if all singleton classes in a third-party open source framework are implemented using the eager initialization pattern, it will initialize all singleton classes, which will undoubtedly be catastrophic.

Lazy Singleton Pattern #

Lazy Singleton pattern is a singleton design pattern that is used to avoid creating objects in advance when directly loading a class object. This pattern uses lazy loading, which means that the instance is only loaded into the heap memory when the system uses the class object. By looking at the following code, we can understand the implementation of lazy loading:

// Lazy Singleton pattern
public final class Singleton {
    private static Singleton instance= null;// do not instantiate
    private Singleton(){}// constructor
    public static Singleton getInstance(){// provide the instance to the entire system through this method
        if(null == instance){// if instance is null, instantiate it; otherwise, directly return the object
            instance = new Singleton();// instantiate the object
        }
        return instance;// return the existing object
    }
}

The above code works fine in a single-threaded environment. However, in a multi-threaded environment, multiple instances may be created. Why does this happen?

When thread A enters the if condition, it starts instantiating the object while instance is still null. Then, thread B enters the if condition and goes into the method to create another instance.

To ensure that only one instance is created in a multi-threaded scenario, we need to add synchronization to this method. Here, we use the synchronized keyword to synchronize the getInstance method:

// Lazy Singleton pattern + synchronized
public final class Singleton {
    private static Singleton instance= null;// do not instantiate
    private Singleton(){}// constructor
    public static synchronized Singleton getInstance(){// synchronize the method to provide the instance to the entire system
        if(null == instance){// if instance is null, instantiate it; otherwise, directly return the object
            instance = new Singleton();// instantiate the object
        }
        return instance;// return the existing object
    }
}

However, as we mentioned earlier, adding synchronization will lead to lock contention and reduce system performance, thus decreasing the performance of the singleton pattern.

Moreover, every time we request the class object, it is mostly not null except for the first time. Before adding synchronization, multiple instances are created because the if condition is satisfied when it is null. Based on these two points, we can consider placing the synchronization inside the if condition to reduce synchronization lock contention.

// Lazy Singleton pattern + synchronized
public final class Singleton {
    private static Singleton instance= null;// do not instantiate
    private Singleton(){}// constructor
    public static Singleton getInstance(){// synchronize the method to provide the instance to the entire system
        if(null == instance){// if instance is null, instantiate it; otherwise, directly return the object
          synchronized (Singleton.class){
              instance = new Singleton();// instantiate the object
          } 
        }
        return instance;// return the existing object
    }
}

You may think this is sufficient, right? The answer is that multiple instances can still be created. This is because when multiple threads enter the if condition, although there is synchronization, the threads that enter the if condition one by one will still acquire the lock, create an object, and then release the lock. Therefore, we need to add another condition inside the synchronization lock:

// Lazy Singleton pattern + synchronized + double-check
public final class Singleton {
    private static Singleton instance= null;// do not instantiate
    private Singleton(){}// constructor
    public static Singleton getInstance(){// synchronize the method to provide the instance to the entire system
        if(null == instance){// first condition: if instance is null, instantiate it; otherwise, directly return the object
          synchronized (Singleton.class){// synchronization lock
             if(null == instance){// second condition
                instance = new Singleton();// instantiate the object
             }
          } 
        }
        return instance;// return the existing object
    }
}

By adding the double-check condition, the creation of multiple instances can be avoided. } }

The above approach is usually called Double-Check and can greatly improve the performance of lazy initialization in a multi-threaded environment. Does this mean that it guarantees no errors? Are there any other issues?

In fact, this is related to the Happens-Before rule and reordering. Let’s briefly understand the Happens-Before rule and reordering.

In a previous [extra] article, we shared that compilers, in order to minimize the number of reads and stores to registers, will fully reuse the stored values of registers. For example, in the following code, without reordering optimization, the normal execution order is steps 1/2/3. However, after reordering optimization during compilation, the execution steps may become steps 1/3/2, reducing the number of register accesses:

int a = 1;  // Step 1: Load the memory address of variable a into a register, load 1 into the register, and write 1 to the memory specified by the register using the mov instruction of the CPU
int b = 2;  // Step 2: Load the memory address of variable b into a register, load 2 into the register, and write 2 to the memory specified by the register using the mov instruction of the CPU
a = a + 1;  // Step 3: Reload the memory address of variable a into a register, load 1 into the register, and write 1 to the memory specified by the register using the mov instruction of the CPU

In JMM (Java Memory Model), reordering is crucial, especially in concurrent programming. If the JVM can reorder them arbitrarily to improve program performance, it may also bring a series of problems to concurrent programming. For example, the singleton problem in the Double-Check pattern I mentioned earlier. Assuming the class has other properties that also need to be instantiated, in addition to instantiating the singleton class itself:

// Lazy initialization + synchronized lock + double-check
public final class Singleton {
    private static Singleton instance = null;  // Do not instantiate
    public List<String> list = null;  // List property
    private Singleton() {
        list = new ArrayList<String>();
    }  // Constructor
    public static Singleton getInstance() {  // Provide instance to the entire system through this function with the synchronized lock
        if (null == instance) {  // First check, instantiate the object when instance is null, otherwise return the object directly
            synchronized (Singleton.class) {  // Synchronized lock
                if (null == instance) {  // Second check
                    instance = new Singleton();  // Instantiate the object
                }
            }
        }
        return instance;  // Return the existing object
    }
}

In the execution of the instance = new Singleton(); code, under normal circumstances, the instantiation process is as follows:

  • Allocate memory for Singleton.
  • Call the constructor of Singleton to initialize the member variables.
  • Assign the Singleton object to the allocated memory space (after this step, singleton is non-null).

If the virtual machine performs reordering optimization, at this time step 3 may occur before step 2. If the initialization thread just completed step 3 and step 2 has not been executed, then another thread has reached the first check. At this time, the check is non-null and returns the object for use. However, in this case, the construction of other properties has not been completed, so using these properties may cause exceptions. Here, Synchronized can only ensure visibility and atomicity, but cannot guarantee the execution order.

At this point, the importance of the Happens-Before rule is reflected. By its literal meaning, you may mistakenly think that the previous operation occurs before the subsequent operation. However, the true meaning is that the result of the previous operation can be obtained by subsequent operations. This rule specifies the reordering optimization of compilers on programs.

We know that the volatile keyword can ensure the visibility of variables between threads, which means that when thread A modifies variable X, other threads executed after thread A can see the change in variable X. In addition, after JDK 1.5, volatile has another role, which is preventing local reordering. In other words, the instructions of volatile variables will not be reordered. So, after using volatile to modify instance, the Double-Check lazy initialization pattern is foolproof:

// Lazy initialization + synchronized lock + double-check
public final class Singleton {
    private volatile static Singleton instance = null;  // Do not instantiate
    public List<String> list = null;  // List property
    private Singleton() {
        list = new ArrayList<String>();
    }  // Constructor
    public static Singleton getInstance() {  // Provide instance to the entire system through this function with the synchronized lock
        if (null == instance) {  // First check, instantiate the object when instance is null, otherwise return the object directly
            synchronized (Singleton.class) {  // Synchronized lock
                if (null == instance) {  // Second check
                    instance = new Singleton();  // Instantiate the object
                }
            }
        }
        return instance;  // Return the existing object
    }
}

Implementation using Inner Class #

The above-mentioned implementation using synchronized lock and Double-Check is relatively complex and involves the use of synchronized locks. Is there a simpler way to achieve thread safety and lazy loading?

As we know, in the eager initialization pattern, we use the static modifier to declare the instance variable “instance”. This variable will be collected into the class constructor during the class initialization process. In a multi-threaded scenario, the JVM ensures that only one thread can execute the class constructor method, while other threads will be blocked and wait. This approach guarantees memory visibility, ordering, and atomicity.

If we create an inner class in the Singleton class to implement the initialization of the instance variable, we can avoid the situation where multiple objects are created under multiple threads. In this approach, the InnerSingleton class will only be loaded and the object will be instantiated when the getInstance() method is first called. The specific implementation is as follows:

// Lazy initialization using inner class
public final class Singleton {
    public List<String> list = null; // list attribute
    
    private Singleton() { // constructor
        list = new ArrayList<String>();
    }
    
    // Inner class implementation
    public static class InnerSingleton {
        private static Singleton instance = new Singleton(); // create instance
        
    }
    
    public static Singleton getInstance() {
        return InnerSingleton.instance; // return static variable in the inner class
    }
}

Summary #

There are actually many ways to implement singletons, but they can be summarized into two types: eager initialization and lazy initialization. We can choose according to our own needs.

If we know for certain that the class will be loaded after the program starts, then using eager initialization to implement the singleton is simple and practical. If we are writing utility classes, then we should prioritize using lazy initialization. This is because in many projects, the utility class may be included in a jar file, but not necessarily used. Implementing the singleton using lazy initialization can prevent it from being loaded into memory in advance and occupying system resources.

Thought Questions #

Are there any other ways you know of to implement a singleton besides the ones mentioned above?