DEV Community

Varun
Varun

Posted on

Singleton Design Pattern

There are many scenarios where we need to restrict the object creation to 1. So, ensure that a class has only one instance and provide a global point of access to it.

Solution: Restrict instantiation of a class to a single object and provide a global access point to that instance.

Now, Real-life Application Examples:

  • Logger: Ensuring only one logger instance exists across the application.
  • DB Connection : It requires only 1 object for db connection
  • Configuration Settings: Having a single object to manage configuration settings.

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. There are several variations of implementing the Singleton pattern in Java, each with its own characteristics and considerations:

  1. Eager Initialization:

    • In this approach, the singleton instance is created eagerly, i.e., at the time of class loading.
    • It guarantees thread safety since the instance is created before any thread can access it.
    • static associates with class, which means the class cannot be reinitialized and all instances of class share same static variable
    • getInstance() Method exposed as public will return same object also static so only 1 object is returned.

      public class Singleton {
          private static final Singleton instance = new Singleton();
      
          private Singleton() {} // Private constructor
      
          public static Singleton getInstance() {
              return instance;
          }
      }
      
      
  2. Lazy Initialization :

    • In this approach, the singleton instance is created only when needed, i.e., on the first call to getInstance().
    • Now when instance is equal to null then only new object is created.
    public class Singleton {
        private static Singleton instance;
    
        private Singleton() {} // Private constructor
    
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    Problem: But It is not thread-safe, meaning multiple threads can potentially create multiple instances if called simultaneously.
    Suppose there 2 threads coming at same time they both will check that its null and create 2 objects.

  3. Synchronized Method:

  • This approach improves performance by avoiding synchronization after the singleton instance is created.
  • Synchronized puts lock when one thread comes and creates instance it locks for other threads.

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {} // Private constructor
    
          synchronized public static Singleton getInstance() {
                    if (instance == null) {
                        instance = new Singleton();
                    }
            return instance;
        }
    }
    

    Problem: Its very expensive when 1000 threads come, 1st will lock create, 2 will lock, 3rd lock every time it will lock.

  1. Double Locking

    • It uses double-checked locking to ensure thread safety, but it's susceptible to the "Double-Checked Locking Problem" in Java versions prior to Java 5.

    Working: When 2 threads come at same time in initially as null, so they both enter into if condition, because of synchronized only t1 will be allowed then it checks object is null and creates the object now t2 comes it denies that object==null and goes out and returns the object.

        public class Singleton {
            private static volatile Singleton instance;

            private Singleton() {} // Private constructor

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

Enter fullscreen mode Exit fullscreen mode

Problems:

  1. Reordering of instruction:

Suppose we are creating new variable with member variable value, like
instance = new Singleton(10);
so during this the process are

  1. allocation of memory
  2. Initialize Member variable if not available
  3. assign reference of memory to object

now for performance improvement CPU reorders these steps, so it can assign the reference of memory before initializing, and it will be assigned some random compiler value.
Now even before initializing variable with value 10, the object will be pointed to memory pointer, and for 2nd thread when it comes in 1st if for object==null it will be false as its got some random value. and it will return same object.

  1. Memory Issue: Suppose 1 thread has started the process to allocate memory and creation of object but at same time thread 2 accessed by another core.
    Now look this structure.

        | Core1 | Core2 |
        | --- | --- |
        | Cache1 | Cache2 |
        | Common Memory Common Memory |
    

    But when thread 2 accesses the core, both the cache was not synced so it will check for obj==null which will be true and so it will ask to memory which was not synced by cache, and will create another Object.

  2. Using Volatile:

    using the volatile keyword is a solution to address some of the potential issues associated with the double-checked locking pattern. By declaring the reference to the singleton instance as volatile, you ensure that changes to the instance are immediately visible to all threads. This helps prevent the visibility problem that can occur due to optimizations performed by the Java Virtual Machine (JVM) and CPU.

    javaCopy code
    public class Singleton {
        private static volatile Singleton instance; // Declare as volatile
    
        private Singleton() {} // Private constructor
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    

    With the volatile keyword, changes made to the instance variable are immediately visible to other threads, ensuring that they see the fully initialized singleton instance. This helps prevent the possibility of a partially constructed instance being visible to other threads.

  3. Lazy Initialization with Static Inner Class (Thread-Safe, Post-Java 5):

    • This approach provides thread safety with lazy initialization and is guaranteed to work in all versions of Java.
    • It leverages the behavior of static inner classes to ensure lazy loading and thread safety.

      public class Singleton {
          private Singleton() {} // Private constructor
      
          private static class SingletonHolder {
              private static final Singleton INSTANCE = new Singleton();
          }
      
          public static Singleton getInstance() {
              return SingletonHolder.INSTANCE;
          }
      }
      
      

Each approach has its trade-offs in terms of thread safety, performance, and compatibility. Choosing the appropriate Singleton implementation depends on the specific requirements of your application.

Top comments (0)