Singleton design pattern is a creational design pattern that restricts instantiation of a class ensuring class has only one instance and provide a global point of access to it.

Common Implementations

  • Lazy Initialization: ensures instance is created only when it is first requested.
  • Eager Initialization: instance is created when the class is loaded.
  • Thread-Safe Implementations: ensures the singleton works correctly in multi-threaded environments, such as using synchronized methods or double-checked locking. (though double-checked locking has nuances and is sometimes considered an anti-pattern in modern Java)

Use Cases

  • Logging
  • Configuration settings
  • Thread pools
  • Caches
  • Device drivers

Examples

Eager initialization

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

    private EagerInitializedSingleton(){}

    public static Singleton getInstance() {
        return instance;
    }
}
  • Useful if your singleton class is not using a lot of resources.

Using synchronized method (Lazy initialization)

public class Singleton {

    // Volatile ensures visibility and prevents reordering
    private static volatile Singleton instance;

    private Singleton() {
    }

    // Thread-safe with method-level synchronization
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public void example() {
        System.out.println("Hello from Singleton!, (using synchronized method)");
    }
}
  • Lazily initialization the instance.
  • synchronized ensures that only one thread can enter the method at a time.
  • It ensures lazy initialization — Singleton instance is created only when needed.
  • Performance overhead: Every call to getInstance() acquires a lock, even after the instance has been initialized. This is unnecessary and can be expensive under heavy load.

Double-Checked Locking (Lazy initialization)

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) { // <- Double-check
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public void example() {
        System.out.println("Hello from Singleton!, (double-check)");
    }

}
  • Lazily initialization the instance.
  • double check aims to minimize the overhead of synchronization.
  • Complex
public class Singleton {
    private Singleton() {
        // Prevent reflection attack
        if (SingletonHelper.INSTANCE != null) {
            throw new RuntimeException("Use getInstance() method to get the single instance of this class");
        }
    }

    private static class SingletonHelper {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public void example() {
        System.out.println("Hello from Singleton!, (Bill Pugh)");
    }
}
  • Static inner helper class is loaded only when getInstance() is called.
  • Class loading is thread-safe.
  • No need for synchronized, and lazy initialization is achieved.

Drawbacks

Despite its utility, the Singleton pattern is sometimes criticized as an anti-pattern because it can:

  • Creates global state: Leading to tight coupling and potential side effects.
  • Makes Testability difficult: Making unit testing challenging due to hidden dependencies.
  • Violate the Single Responsibility Principle: When the singleton takes on responsibilities beyond managing its own instance.