Skip to main content

Design Patterns

Singleton Design pattern

Definition

The Singleton design pattern ensures a class has only one instance and provides a global access point to it. This means you control object creation and guarantee there’s only ever a single instance throughout your application.

Applications

  • Use the Singleton pattern when a class in your program should have just a single instance available to all clients; for example, a single database object shared by different parts of the program.
  • Database Connections: A Singleton can control access to a single database connection, ensuring efficient resource utilization and avoiding conflicts.
  • File Handles: Similarly, a Singleton can manage file handles, preventing multiple processes from modifying the same file simultaneously.
  • Thread Pools: A thread pool Singleton provides a central location to manage a limited number of threads, optimizing resource allocation for tasks.
  • Configuration Settings: A Singleton can store and provide access to application-wide configuration settings, simplifying access for various parts of your code.
  • Logging: A Singleton logger can centralize logging functionalities and ensure consistent output format across the application.

Implementations in java

1. Eager Initialization:

This approach creates the instance as soon as the class is loaded. It’s simple but may waste resources if the object isn’t used immediately.

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

  private EagerSingleton() {}

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

2. Lazy Initialization:

The instance is created only when it’s first requested. This saves resources but requires synchronization for thread safety.

public class LazySingleton {
  private static LazySingleton instance;

  private LazySingleton() {}

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

3. Synchronized Lazy Initialization:

The Singleton instance is created only when it’s requested for the first time. It uses a synchronized method to ensure thread safety.

This approach uses Lazy initialization, and is thread-safe but the synchronization can introduce performance overhead.

public class SynchronizedLazySingleton {
  private static LazySingleton instance;

  private SynchronizedLazySingleton() {}

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

4. Double checked locking:

This is thread safe and provide bettern performance than previous solution

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;
    }
}

5. Bill Pugh Singleton or Static holder to create singleton:

he Singleton instance is created when the inner class is first referenced. This ensures that the instance is only created when it is needed, which is a form of lazy initialization.

public class StaticHolderSingleton {
    // Private constructor to prevent external instantiation
    private StaticHolderSingleton() {
        // Initialization code here
    }

    // Inner static class responsible for lazy initialization
    private static class SingletonHelper {
        // Static final instance of the Singleton
        private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
    }

    // Public method to access the Singleton instance
    public static StaticHolderSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

6. Singleton using enum

Enums in Java offer more than just constants:

  • They can hold and execute methods, making them versatile tools.
  • They are inherently thread-safe because:
  • Enum values act like singletons — only one instance exists for each value.
  • These instances are created only once when the enum class loads, and you can’t create new ones.

Enums as Singletons:

  • Using enums for the Singleton pattern is a clean and efficient approach.
  • It avoids issues common with other Singleton implementations:
  • Reflection can’t be used to create new enum instances.
  • Serialization is handled automatically by the JVM for enums.

In essence, enums provide a robust and convenient way to implement the Singleton pattern in Java.

public enum EnumSingleton {
  INSTANCE;

  // You can add methods to encapsulate behavior
  public void doSomething() {
    System.out.println("Doing something in the Singleton");
  }
}

Cautions Before Using Singletons:

  1. State Management Challenges: Singletons can introduce global state that’s difficult to manage. Changes to the Singleton’s state can have unintended side effects throughout your codebase. If complex state management is required, explore alternatives like dependency injection for better control.
  2. Testing Difficulties: Unit testing classes that rely on Singletons can be tricky. Mocking or dependency injection techniques might be necessary to isolate the class under test and avoid relying on the global state of the Singleton.
  3. Tight Coupling: Singletons create tight coupling between classes that depend on the Singleton instance. This can make code less modular and harder to maintain. Changes to the Singleton might ripple through the entire codebase. Dependency injection can help promote loose coupling and improve code maintainability.
  4. Overuse: Singletons can be tempting to overuse, leading to a less flexible and more complex codebase. Consider if there’s a simpler way to achieve the desired functionality without a Singleton. Alternative patterns like factory methods or dependency injection might be better suited for specific scenarios.
  5. Reflection and Serialization Issues: Depending on the implementation, other Singleton approaches might be vulnerable to attacks through reflection (creating new instances) or serialization problems (restoring state incorrectly). Enums, on the other hand, are inherently resistant to these issues.

In Conclusion:

Singletons can be useful in specific situations, but their drawbacks should be carefully weighed. Consider alternatives like dependency injection and other design patterns for promoting loose coupling, testability, and maintainable code. If you do choose to use Singletons, be mindful of the potential pitfalls and use them judiciously.