Singleton Design Pattern: Detailed Explanation and Practical Examples

Singleton Design Pattern

The Singleton Design Pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when exactly one object is needed to coordinate actions across the system.

Key Concepts

  1. Single Instance: Ensures a class has only one instance.
  2. Global Access: Provides a global point of access to the instance.
  3. Thread Safety: Ensures the Singleton instance is created in a thread-safe manner.

Implementations
 

1. Basic Singleton

A basic Singleton implementation in Java.

public class BasicSingleton {
    private static BasicSingleton instance;
    private BasicSingleton() { }
    public static BasicSingleton getInstance() {
        if (instance == null) {
            instance = new BasicSingleton();
        }
        return instance;
    }
    public static void main(String[] args) {
        BasicSingleton singleton1 = BasicSingleton.getInstance();
        BasicSingleton singleton2 = BasicSingleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

Usage

public class SingletonDemo {
    public static void main(String[] args) {
        BasicSingleton singleton = BasicSingleton.getInstance();
    }
}

Expected Output: Both singleton1 and singleton2 will have the same hash code because they reference the same instance.

12345678
12345678

2. Thread-Safe Singleton with Synchronized Method

Making the getInstance method synchronized ensures thread safety by allowing only one thread to execute it at a time. To make the Singleton thread-safe, the getInstance method can be synchronized:

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    private ThreadSafeSingleton() { }
    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
    public static void main(String[] args) {
        ThreadSafeSingleton singleton1 = ThreadSafeSingleton.getInstance();
        ThreadSafeSingleton singleton2 = ThreadSafeSingleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

Expected Output: Both singleton1 and singleton2 will have the same hash code.

12345678
12345678

Drawback: Synchronizing the method can significantly reduce performance due to the overhead of acquiring and releasing the lock every time the method is called.

3. Double-Checked Locking Singleton

This approach minimizes synchronization overhead by checking the instance twice, once outside and once inside the synchronized block.

Example

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;
    private DoubleCheckedLockingSingleton() { }
    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) {
        DoubleCheckedLockingSingleton singleton1 = DoubleCheckedLockingSingleton.getInstance();
        DoubleCheckedLockingSingleton singleton2 = DoubleCheckedLockingSingleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

Explanation: The volatile keyword ensures that changes to the instance variable are visible to all threads.

Expected Output: Both singleton1 and singleton2 will have the same hash code.

12345678
12345678

4. Bill Pugh Singleton

This approach uses a static inner helper class to create the Singleton instance. The instance is created only when the getInstance method is called, and the class loader mechanism ensures thread safety.

Example

public class BillPughSingleton {
    private BillPughSingleton() { }
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
    public static void main(String[] args) {
        BillPughSingleton singleton1 = BillPughSingleton.getInstance();
        BillPughSingleton singleton2 = BillPughSingleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

Explanation: The inner static class is not loaded until the getInstance method is called, ensuring lazy initialization.

Expected Output: Both singleton1 and singleton2 will have the same hash code.

12345678
12345678

Breaking Singleton Pattern
 

Serialization

Serialization can break the Singleton pattern by creating a new instance during deserialization.

Example

import java.io.*;
public class SerializedSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final SerializedSingleton instance = new SerializedSingleton();
    private SerializedSingleton() { }
    public static SerializedSingleton getInstance() {
        return instance;
    }
    protected Object readResolve() {
        return getInstance();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerializedSingleton singleton1 = SerializedSingleton.getInstance();

        // Serialize the singleton instance
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"))) {
            out.writeObject(singleton1);
        }
        // Deserialize the singleton instance
        SerializedSingleton singleton2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"))) {
            singleton2 = (SerializedSingleton) in.readObject();
        }
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

Explanation: The readResolve method returns the existing instance, preventing the creation of a new instance during deserialization.

Expected Output: Both singleton1 and singleton2 will have the same hash code because readResolve ensures the same instance is returned.

12345678
12345678

Reflection

Reflection can break the Singleton pattern by accessing the private constructor.

Example

import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
    public static void main(String[] args) {
        BillPughSingleton instanceOne = BillPughSingleton.getInstance();
        BillPughSingleton instanceTwo = null;
        try {
            Constructor[] constructors = BillPughSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                constructor.setAccessible(true);
                instanceTwo = (BillPughSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

Expected Output: instanceOne and instanceTwo will have different hash codes because the reflection was used to create a new instance.

12345678
87654321

Protecting Singleton from Reflection
 

Solution

To prevent breaking the Singleton pattern through reflection, throw an exception if the constructor is called more than once.

Example

public class ProtectedSingleton {
    private static boolean instanceCreated = false;
    private static ProtectedSingleton instance;
    private ProtectedSingleton() {
        if (instanceCreated) {
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
        instanceCreated = true;
    }
    public static ProtectedSingleton getInstance() {
        if (instance == null) {
            instance = new ProtectedSingleton();
        }
        return instance;
    }
    public static void main(String[] args) {
        ProtectedSingleton instanceOne = ProtectedSingleton.getInstance();
        ProtectedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = ProtectedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                constructor.setAccessible(true);
                instanceTwo = (ProtectedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        if (instanceTwo != null) {
            System.out.println(instanceTwo.hashCode());
        }
    }
}

Expected Output: An exception will be thrown during reflection instantiation, preventing instanceTwo from being created.

12345678
java.lang.RuntimeException: Use getInstance() method to get the single instance of this class.
    at ProtectedSingleton.<init>(ProtectedSingleton.java:8)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    ...

Summary

  • Basic Singleton: Simple but not thread-safe.
  • Thread-Safe Singleton: Uses synchronization to ensure thread safety but can be slow.
  • Double-Checked Locking Singleton: Reduces synchronization overhead while ensuring thread safety.
  • Bill Pugh Singleton: Leverages class loader mechanism for lazy initialization and thread safety.
  • Breaking Singleton: Handled by serialization safeguards and reflection prevention.

By understanding and applying these concepts and implementations, you can effectively use and maintain the Singleton design pattern in your applications, ensuring both functionality and performance while avoiding common pitfalls.