Singleton Design Pattern

Singleton Design Pattern

Photo by Max Böhme on Unsplash

Introduction

This pattern is used when we have to create only 1 instance of the class

There are 4 ways to achieve this

  1. Eager

  2. Lazy

  3. Synchronized Method

  4. Double locking

4 Ways of Singleton Pattern

I Eager Initialization

In this approach, we confine the creation of objects by setting the constructor to private and ensuring all other methods are static. Since static methods pertain to the class rather than individual objects, only a single object is generated and returned. This constitutes eager initialization, as the object is created during class initialization.

package methods;

public class DBConnection {

    private static DBConnection conObject = new DBConnection();

    private DBConnection(){
    }

    public static DBConnection getInstance(){
        return conObject;
    }
}

Usage

package methods;

public class Main {
    public static void main(String[] args) {
        DBConnection conObject = DBConnection.getInstance();
    }
}

II Lazy Initialization

Whenever there arises a need to create an object, we initialize and return it if it hasn't been initialized yet (i.e., if the object is null).

Problem: The issue with lazy initialization arises when two threads concurrently reach the if condition in the getInstance method. Both threads detect that the object is null and proceed to create a new object. Consequently, two objects are created in memory.

package methods;

public class DBConnection {

    private static DBConnection conObject;

    private DBConnection(){
    }

    public static DBConnection getInstance(){
        if (conObject==null){
            conObject = new DBConnection();
        }
        return conObject;
    }
}

III Synchronized (Lock)

When the synchronized keyword is utilized with a method, it enforces a lock on that method. Consequently, when a thread invokes the synchronized method, it obtains the lock and enters the method's execution. If another thread endeavors to access the same method while the initial thread is already executing it, it is unable to proceed due to the lock imposed by synchronization. Subsequently, once the initial thread completes its execution and releases the lock, the subsequent thread verifies whether the object has been created. If not, it proceeds with the creation process.

This is also an example of lazy initialization.

package methods;

public class DBConnection {

    private static DBConnection conObject;

    private DBConnection(){
    }

    synchronized public static DBConnection getInstance(){
        if (conObject==null){
            conObject = new DBConnection();
        }
        return conObject;
    }
}

Problem with synchronized

Locking is a costly operation. If this method is called 100 or 1000 times, we unnecessarily acquire the lock each time, which is inefficient because other processes must wait until the lock is released.

In this scenario, we perform two checks before object creation. If two threads concurrently reach the condition and both ascertain that the object is null, one of the threads acquires the lock, proceeds to create the object, and then releases the lock. Subsequently, the next thread acquires the lock, performs another check to ensure the object is still null, creates it if necessary, or releases the lock and continues. This double locking mechanism is considered the most effective approach and is widely recommended in the industry.

This is also an example of lazy initialization.

package methods;

public class DBConnection {

    private static DBConnection conObject;

    private DBConnection(){
    }

    public static DBConnection getInstance(){
        if (conObject==null){
            synchronized (DBConnection.class){
                if (conObject==null){
                    conObject = new DBConnection();
                }
            }
        }
        return conObject;
    }
}

Bugs in Double Locking

Issue 1 - Reordering of Instruction

To explain this issue, let's consider a sample member variable in the database creation object.

package methods;

public class DBConnection {

    private static DBConnection conObject;
    int memberVariable;

    private DBConnection(int memberVariable){
        this.memberVariable = memberVariable;
    }

    public static DBConnection getInstance(){
        if (conObject==null){
            synchronized (DBConnection.class){
                if (conObject==null){
                    conObject = new DBConnection(10);
                }
            }
        }
        return conObject;
    }
}

Creation of conObject

conObject = new DBConnection(10);

So, generally, what the CPU does for the above statement at a high level has the following three operations:

  1. Allocate memory.

  2. Initialize all the member variables.

  3. Assign the reference of memory to the 'conn' object.

CPU does re ordering of instruction to increase the performance

we zoom into the method of getInstance by expanding it into the 3 step operations.

    public static DBConnection getInstance(){
        if (conObject==null){
            synchronized (DBConnection.class){
                if (conObject==null){
                    memoryPointer = allocateMemory();
                    memoryPointer.memberVariable = initializeVariable();
                    conObject = memoryPointer;
                }
            }
        }
        return conObject;
    }

Reordering of 3 insturctions can happen like the following way

memoryPointer = allocateMemory(); // Step 1
conObject = memoryPointer; // step 2
memoryPointer.memberVariable = initializeVariable(); //step 3

In this scenario, before initializing the member variable, we reordered the step to allocate memory pointer to conObject.

Assume that there are 2 threads. If thread 1 completes step 2 and is about to execute step 3, meanwhile, thread 2 sees that conObject is not null and will skip initialization, starting to use the conObject and its member variable, which has a default value. So, using the default value creates a big issue.

Issue 2 - L1 Caching

In a CPU, each core has its own cache, which is the L1 cache. First, the data processed by a core gets stored in the cache, and then it's stored in memory. These caches also sync up regularly, but there might be delays in the synchronization.

Assuming there are two threads: if thread 1 creates the conObject and merely stores it in the cache without putting it in memory, but the lock has been released by thread 1 which was acquired through synchronization. During this time, thread 2 arrives and discovers that the conObject is still null since it has not yet been updated by thread 1 in memory. Consequently, thread 2 will proceed to create a new object. This results in the creation of two objects, which is not the expected behavior.

Solution

Using VOLATILE Keyword for singleton object

package methods;

public class DBConnection {

    private volatile static DBConnection conObject; // Only change
    int memberVariable;

    private DBConnection(int memberVariable){
        this.memberVariable = memberVariable;
    }

    public static DBConnection getInstance(){
        if (conObject==null){
            synchronized (DBConnection.class){
                if (conObject==null){
                    conObject = new DBConnection(10);
                }
            }
        }
        return conObject;
    }
}

Volatile has 2 properties:

I. It reads and writes from memory.

II. Have Before - If the volatile object is present at a particular line, then all the instructions before the volatile can be grouped and reordered in any possible manner. Similarly, all the instructions present after the volatile keyword can be grouped and reordered in any possible manner. We cannot reorder any instruction from before the volatile and place it after the volatile keyword, and vice versa.

All the write lines will be pushed into memory if the code has volatile key word.