Singelton pattern

  • Ensures that a class has only one instance and provides a global point of access to that instance

  • It restricts an object has only one instance and parts of the application uses the same object.

  • It is needed in scenarios where creating multiple objects of a class might be problamatic like logging, configuration handling or managing database connections

  • ex — If we are in office printer and everyone uses it then the printer might overvhelmed, instead of that we have a print spooler which manages all print jobs . No matter who initiates the print job they all go through one central spooler instance that queues and handles the tasks in order

  • Working of singleton pattern

    • singleton pattern involves following patterns

      • private constructor: prevents instamtoatopm from outside the class

      • static variable: Holds the single instance of the class

      • public static method: provides a global access point to get the instance

  • Approaches to implement singleton patterns

    • eager loading

    • lazy loading

  • Eager loading( early initialisation)

    • the singleton instance is created as soon as the class is loaded, regardless of whether it is ever used

    • Real world analogy

      • a fire extinguisher is always present even if the fire never occurs. Similarly eager loading created the singleton instances upfront just in case it needed

    • Understanding

      • the object is created immediately when the class is loaded.

      • it is always available and inherently thread safe

    • Pros

      • very simple to implement

      • thread safe without any extra handling

    • Cons

      • wastes memory if the instance is never used

      • not suitable for heavy objects

  • Lazy loading (on demand initialisation)

    • the singleton instance is created only when it is needed, the first time getInstance() method is called

    • ex — coffee machine , a coffee machine that only brews coffee when we press the button , it doesn't waste energy or resources until we actually want a cup of coffee.

    • Similarly lazy loading creates the singleton instance only when it is requested

  • Understanding

    • The instance starts as null

    • it is only created when getInstances() is first called.

    • Future calls returns the already created instances.

  • Pros

    • saves memory if the instance is never used.

    • Object creation is deferred untill required

  • Cons

    • Lazy loading is not thread-safe by default. Thus it requires synchronisation in multi threaded environments

  • Thread safety

    • in a single-threaded environment, implementing a singleton is straightforward.

    • However things get complicated in multi-threaded applications, which are very common in modern software (especially web servers, mobile apps etc).

    • The problem

      • lets say two threads simultaneously call getInstance() for the first time in a lazy-loaded singleton.

      • If the instance hasn't been created yet, both threads might pass the null check and end up creating two different instances completely breaking the singleton guarantee

      • this kind of bug is

        • hard to detect

        • sever

        • costly

  • Different ways to achieve thread safety

    • synchronised methods

      • simplest way to ensure thread safety

      • By synchronising thee method that creates the instance, we can prevent multiple threads from creating separate instances at the same time

      • But this approach can lead to performance issues due to overhead of synchronisation

  • What synchronised keyword does?

    • The synchronised keyword ensures that only one thread at a time can execute getInstance() method.

    • This prevents multiple threads from entering the method simultaneously and creating multiple instances

    • Pros

      • simple and easy to implement

      • thread-safe without needing complex logic

    • Cons

      • performance overhead: every call to getInstance() is synchronized, even after the instance is created.

      • may slow down the application in high-concurrency scenarios.

  • Double-checked locking

    • this is more efficient way to achieve thread seafty.

    • Idea is to check if the instance is null before acquiring the lock.

    • If it is then we synchronize the block and check again, this reduces the overhead of synchronisation after the instance has been created

  • Understanding

    • The outer if check avoids synchronization once the instance is created.

    • The inner if inside synchronized ensures that only one thread creates the instance.

    • volatile keyword ensures changes made by one thread are visible to others. Without volatile, one thread might create the Singleton instance, but other threads may not see the updated value due to caching. volatile ensures that the instance is always read from the main memory, so all threads see the most up-to-date version.

    Pros

    • Efficient: Synchronization only happens once, when the instance is created.

    • Safe and fast in concurrent environments.

    Cons

    • Slightly more complex than the synchronized method.

    • Requires Java 1.5 or above due to reliance on volatile.

  • Bill Pugh Singleton (Best Practice for Lazy Loading)

    • highly efficient way to implement the Singleton pattern.

    • uses a static inner helper class to hold the Singleton instance.

    • instance is created only when the inner class is loaded, which happens only when getInstance() is called for the first time.

    • Explanation:

      • The Singleton instance is not created until getInstance() is called.

      • The static inner class (Holder) is not loaded until referenced, thanks to Java's class loading mechanism.

      • It ensures thread safety, lazy loading, and high performance without synchronization overhead.

      Pros

      • Best of both worlds: Lazy + Thread-safe.

      • No need for synchronized or volatile.

      • Clean and efficient.

      Cons

      It is slightly less intuitive for beginners due to the use of a nested static class.

  • Eager loading

    • eager loading does not face thread safety issues

    • This approach avoids thread issues altogether by creating the instance upfront — at the cost of potential memory waste.

    • Thus, it is not a preferred method in most cases but is still a valid option.

Pros of Singleton Pattern

  • Cleaner Implementation: Singleton offers a straightforward and tidy way to manage a single instance of a class, especially when designed with thread safety and simplicity in mind.

  • Guarantees One Instance: This pattern enforces that only one instance of the class can exist, making it ideal for shared resources.

  • Provides a Way to Maintain a Global Resource: It allows centralized access to a global resource or service, which can be useful in managing application-wide configurations or state.

  • Supports Lazy Loading: Many Singleton implementations allow the instance to be created only when it is first accessed, optimizing memory usage and startup performance.


Cons of Singleton Pattern

  • Used with Parameters and Confused with Factory: When a Singleton class requires parameters for instantiation, it may blur lines with the Factory pattern, leading to design confusion.

  • Hard to Write Unit Tests: Since the Singleton holds a global state, it becomes difficult to isolate and mock for unit testing, thus potentially hindering testability.

  • Classes Using It Are Highly Coupled to It: Components that depend on the Singleton become tightly coupled to its implementation, which reduces flexibility and makes code harder to maintain or refactor.

  • Special Cases to Avoid Race Conditions: In multi-threaded environments, care must be taken to avoid race conditions during the instance creation phase, complicating implementation.

  • Violates the Single Responsibility Principle (SRP): A Singleton often handles both instance control and its core functionality, thereby violating the SRP, a key principle of clean software design.


Conclusion

The Singleton pattern can be a powerful tool when used appropriately, particularly for managing global states and shared resources. However, developers should be mindful of its drawbacks, especially regarding testing and maintainability. Consider alternatives or enhanced implementations (like dependency injection) where appropriate to maintain clean and scalable codebases.

Last updated