CLOSE

What is the Singleton Design Pattern?

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it.

It is commonly used for:

  • Configuration managers
  • Logging systems
  • Database connections
  • Thread pools

In simpler terms:

Imagine you are building an application where you only want one shared object throughout the lifecycle of the program. This is where Singleton comes into play – it restricts object creation and guarantees that all parts of your application use the same object.

The Problem It solves

In a typical application, creating multiple objects of a class might not be problematic. However, in certain scenarios – like logging, configuration handling, or managing a database connection – you want just one instance to avoid redundancy, excessive memory use, or inconsistent behavior.

When to use Singleton Method Design Pattern?

Use the Singleton method Design Pattern when:

  1. There must be exactly one instance of a class and it must be accessible to clients from a well-known access point:
    1. This requirement encapsulates the essence of the Singleton pattern. It ensures that only one instance of the class exists, providing a global point of access to it. For example, in a game application, there might be a need for a single instance of a GameManager class to manage game states and resources.
  2. When the sole instance should be extensible by subclassing and clients should be able to use an extended instance without modifying:
    1. This implies that the Singleton instance should support inheritance, allowing clients to use extended versions without altering their code. This requirement can be fulfilled by designing the Singleton class with protected or virtual methods that subclasses can override. For instance, in a GUI framework, a WindowManager Singleton class might be extended to support additional window types or behaviors.
  3. Singleton classes are used for logging, driver objects, caching, thread pool, and database connections:
    1. These are classic examples where the Singleton pattern proves its worth.
      1. Logging: A Logger Singleton class ensures that all parts of the application log messages using the same logger instance, maintaining consistency in log formatting and destination.
      2. Driver Objects: For hardware interaction, a Singleton representing the hardware driver ensures that there's only one instance managing device communication.
      3. Caching: A CacheManager Singleton provides a central point for caching frequently used data, optimizing performance by reducing redundant computations.
      4. Thread Pool: A ThreadPool Singleton manages a pool of worker threads, ensuring efficient utilization of system resources in multi-threaded applications.
      5. Database Connections: A DatabaseConnection Singleton manages database connections, ensuring that all database operations share the same connection, reducing overhead and ensuring transaction consistency.

In all these cases, the Singleton pattern ensures that there's only one instance of the class, providing a centralized point of access for clients while managing global resources efficiently. It promotes code maintainability, scalability, and flexibility, making it a valuable design pattern in various software engineering scenarios.

Implementation in C++

#include <iostream>

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

    // Static instance of the class
    static Singleton* instance;

public:
    // Method to access the Singleton instance
    static Singleton* getInstance() {
        // Lazy initialization: create instance only when needed
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // Example method to demonstrate functionality
    void showMessage() {
        std::cout << "Hello, I am a Singleton instance!" << std::endl;
    }
};

// Initializing static member instance
Singleton* Singleton::instance = nullptr;

int main() {
    // Accessing Singleton instance
    Singleton* singletonInstance = Singleton::getInstance();
    singletonInstance->showMessage();

    return 0;
}

Key Points:

  1. Private Constructor:
    1. The constructor of the Singleton class is private, preventing the instantiation of the class from outside.
  2. Static Instance:
    1. A static member variable instance of type Singleton* holds the sole instance of the class.
  3. getInstance() Method:
    1. The getInstance() method provides a static point of access to the Singleton instance.
    2. It follows the lazy initialization approach, creating the instance only when it's first accessed.
  4. Usage:
    1. Clients can access the Singleton instance using Singleton::getInstance().

Why Is It a Creational Pattern?

The Singleton Pattern falls under the creational design patterns. This is because it deals with how objects are created. Unlike simple instantiation (new), Singleton controls the object creation process by returning an existing instance rather than creating a new one.

Identifying the Need for a Singleton

Imagine you are developing a loggin service. You need a class that writes logs to a file. If every part of your application creates a new logger instance, the result might be:

  • Overwritten logs
  • Multiple file handles
  • Synchronization issues

Instead, if there's only one logger instance (a Singleton_, all parts of the program write to the same log file in a controlled manner.

Working of Singleton Pattern

The Singleton Pattern typically involves the following steps:

  1. Private Constructor
    1. Prevents external code from using new to create objects.
    2. Only the class itself can construct the object.
  2. Static Instance Variable
    1. Holds the single instance of the class.
    2. Shared across all uses of the class.
  3. Public Static Method (getInstance())
    1. Checks if the instance exists.
    2. If not, creates it.
    3. Always returns the same instance.

Real-Life Analogy

Imagine a remote control for your air conditioner.

There is only one remote, and everyone uses it to interact with the AC.

That's the Singleton pattern – one shared instance used by all.

Approaches to Implement Singleton Pattern

In the real world, while designing the product, there are two primary ways to implement the Singleton pattern.

  • Eager Loading
  • Lazy Loading

Each with its own trade-offs in terms of performance, memory usage, and thread safety.

1️⃣ Eager Loading (Early Initialization)

In Eager Loading, the Singleton instance is created as soon as the class is loaded, regardless of whether it's ever used. Let's understand this with a real-life analogy.

  • Object is created at the start of the program.

Real-World Analogy:

Fire Extinguisher in a Building
A fire extinguisher is always present, even if a fire never occurs. Similarly, eager loading creates the Singleton instance upfront, just in case it's needed.

Example Code:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();

Understanding:

  • The object is created immediately when the class is loaded.
  • It's always available and inherently thread-safe.

Pros:

  • Simple to implement.
  • thread-safe without any extra handling.

Cons:

  • Wastes memory if the instance is never used.
  • Not suitable for heavy objects.

2️⃣ Lazy Loading (On-Demand Initialization)

In Lazy Loading, the Singleton instance is created only when it's needed – the first time the getInstance() method is called.

  • Object is created only when it's needed.

Real-World Analogy:

Coffee Machine
Imagine a coffee machine that only brews coffee when you press the button. It doesn't waste energy or resources until you actually want a cup. Similarly, lazy loading creates the Singleton instance only when it's requested.

Example Code:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if (!instance)
            instance = new Singleton();
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

Understanding:

  • The instance starts as null
  • It is only created when getInstance() is the first called.
  • Future calls returns the already created instance.

Pros:

  • Saves memory if the instance is never used.
  • Object creation is deferred until required

Cons:

  • Lazy Loading is Not thread-safe by default. Thus, it requires synchronization in multi-threaded environment.

Thread Safety: A Critical Concern in Singleton Pattern

In a single-threaded environment, implementing a Singleton is straightforward. However, things get complicated in multi-threaded application, which are very common in modern software (especially web servers, mobile apps, etc.).

The Problem:

Let's say two threads simultaneously call getInstance() for the first time in a lazy-loaded Singleton. If the instance has not 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 bugs is:

  • Hard to detect, as it may not occur every time.
  • Severe, because it defeats the whole purpose of the pattern.
  • Costly, especially if the Singleton manages critical resources like logging, configuration, or DB connections.

Different Ways to Achieve Thread Safety

There are several ways to make the Singleton pattern thread-safe. Here are a few common approaches:

1️⃣ Synchronized Method

This is the simplest way to ensure thread safety. By synchronizing the method that creates the instance, we can prevent multiple threads from creating separate instances at the same time. However, this approach can lead to performance issues due to the overhead of synchronization.

Consider the following code snippet for better understanding:

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (!instance)
            instance = new Singleton();
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

2️⃣ Double-Checked Locking

Reduces overhead by locking only during the first creation.

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if (!instance) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!instance)
                instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;