Singleton Design Pattern


Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance.

The Singleton design pattern is one of the most commonly used patterns and a favorite topic in interviews. It opens up numerous follow-up questions, diving deeper into the intricacies of design patterns, coding skills, and multithreading—critical concepts for real-world applications.

Singletone design pattern is used only when only one instance of a class is required throughout the application. This instance is created upon its first access, and the same instance continues to be used until the application terminates.

Scenario Where Singleton is Required

Singleton is useful in situations where only one instance of a class should exist throughout the application's lifecycle. Common scenarios include:

Different Types of Singleton Design Patterns

  1. Eager Initialization Singleton
  2. Lazy Initialization Singleton
  3. Breaking Singleton with Reflection
  4. Thread-Safe Singleton
  5. Static Block Initialization Singleton
  6. Lazy Initialization with Double-Checked Locking

1. Eager Initialization


        public class EagerSingleton {
            private static final EagerSingleton instance = new EagerSingleton();
      
            private EagerSingleton() {
                // Private constructor to prevent instantiation
            }
      
            public static EagerSingleton getInstance() {
                return instance;
            }
        }
        

Drawbacks:

The instance is created even if the client application doesn't use it, which can be problematic. This is especially concerning if the class is establishing a database connection, file handle, or socket. If not managed correctly, it can lead to memory leaks. In such cases, it's better to use the lazy initialization pattern.

2. Lazy Initialization


        public class LazySingleton {
            private static LazySingleton instance;

            private LazySingleton() {
                // Private constructor to prevent instantiation
            }

            public static LazySingleton getInstance() {
                if (instance == null) {
                    instance = new LazySingleton();
                }
                return instance;
            }
        }
    

Drawbacks:

The instance is created only when needed, improving performance. However, this approach is not thread-safe. Multiple threads can create different instances if called simultaneously.

3. Breaking Singleton with Reflection

Sample code for breaking singleton with Reflection

    import java.lang.reflect.Constructor;

    // Simple Singleton class
    class Singleton {
        private static final Singleton instance = new Singleton();

        // Private constructor to prevent instantiation
        private Singleton() {
            // Private constructor
        }

        public static LazySingleton getInstance() {
            if (instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }

    public class ReflectionBreakingSingleton {
        public static void main(String[] args) {
            try {
                // Getting the Singleton instance normally
                Singleton instanceOne = Singleton.getInstance();

                // Using reflection to access the private constructor and create a new instance
                Constructor constructor = Singleton.class.getDeclaredConstructor();
                constructor.setAccessible(true); // Bypassing private access

                // Creating a new instance using reflection
                Singleton instanceTwo = constructor.newInstance();

                // Comparing both instances
                System.out.println("Instance One: " + instanceOne.hashCode());
                System.out.println("Instance Two: " + instanceTwo.hashCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

Prevent singleton failure due to reflection

        public class Singleton {
            private static Singleton instance;

            private Singleton() {
                if (instance != null) {
                    throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
                }
            }

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

Drawbacks:

Using reflection can break Singleton, leading to multiple instances. Fixing this requires preventing reflection or using enum for Singleton implementation.

4. Thread-Safe Singleton

Sample code for multi-thread access the Lazy initialization

            class Singleton {
                private static Singleton instance = null;
            
                // Private constructor to prevent instantiation from other classes
                private Singleton() {
                    System.out.println("Singleton Instance Created");
                }
            
                // Lazy initialization method to return the singleton instance
                public static Singleton getInstance() {
                    if (instance == null) {  // Check if instance is null
                        try {
                            // Simulate a delay to make multithreading issue more apparent
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        instance = new Singleton(); // Create the instance if it doesn't exist
                    }
                    return instance;
                }
            }
            
            public class MultiThreadedSingletonTest {
            
                public static void main(String[] args) {
                    // Creating multiple threads to test the Singleton
                    Thread thread1 = new Thread(() -> {
                        Singleton singleton1 = Singleton.getInstance();
                        System.out.println("Thread 1 instance: " + singleton1.hashCode());
                    });
            
                    Thread thread2 = new Thread(() -> {
                        Singleton singleton2 = Singleton.getInstance();
                        System.out.println("Thread 2 instance: " + singleton2.hashCode());
                    });
            
                    // Start both threads
                    thread1.start();
                    thread2.start();
                }
            }
        

Solution :

            public class ThreadSafeSingleton {
                private static ThreadSafeSingleton instance;
                private static final Object lock = new Object();
                private ThreadSafeSingleton() {
                    // Private constructor to prevent instantiation
                }

                public static ThreadSafeSingleton getInstance() {
                    if (instance == null) {
                        synchronized (lock) {
                            if (instance == null) {
                                instance = new ThreadSafeSingleton();
                            }
                        }
                    }
                    return instance;
                }
            }
        

Drawbacks:

5. Static Block Initialization Singleton


            public class StaticBlockSingleton {
                private static StaticBlockSingleton instance;

                private StaticBlockSingleton() {
                    // Private constructor to prevent instantiation
                }

                // Static block to initialize the Singleton instance
                static {
                    try {
                        instance = new StaticBlockSingleton();
                    } catch (Exception e) {
                        throw new RuntimeException("Exception occurred in creating singleton instance");
                    }
                }

                public static StaticBlockSingleton getInstance() {
                    return instance;
                }
            }
        

Drawbacks:

6. Lazy Initialization with Double-Checked Locking Singleton


            class DoubleCheckedLockingSingleton {

                // Volatile keyword ensures changes made in one thread are visible to others
                private static volatile DoubleCheckedLockingSingleton instance;
            
                // Private constructor to prevent instantiation
                private DoubleCheckedLockingSingleton() {
                    System.out.println("Singleton instance created");
                }
            
                // Method to return the Singleton instance
                public static DoubleCheckedLockingSingleton getInstance() {
                    if (instance == null) {  // First check (no locking)
                        synchronized (DoubleCheckedLockingSingleton.class) {
                            if (instance == null) {  // Second check (with locking)
                                instance = new DoubleCheckedLockingSingleton();
                            }
                        }
                    }
                    return instance;
                }
            }
            
            public class DoubleCheckedLockingSingletonTest {
                public static void main(String[] args) {
                    // Simulate multithreading environment
                    Thread thread1 = new Thread(() -> {
                        DoubleCheckedLockingSingleton singleton1 = DoubleCheckedLockingSingleton.getInstance();
                        System.out.println("Thread 1: Instance hashCode: " + singleton1.hashCode());
                    });
            
                    Thread thread2 = new Thread(() -> {
                        DoubleCheckedLockingSingleton singleton2 = DoubleCheckedLockingSingleton.getInstance();
                        System.out.println("Thread 2: Instance hashCode: " + singleton2.hashCode());
                    });
            
                    thread1.start();
                    thread2.start();
                }
            }