DEV Community

Mukul Saini
Mukul Saini

Posted on

Singleton Design Pattern in Java

The Singleton design pattern is one of the most widely recognized and used patterns in software engineering. It originated from the concept of ensuring that a class has only one instance, an idea inspired by real-world scenarios like government systems requiring a single registry or resource manager. The pattern was first formally described in the seminal book "Design Patterns: Elements of Reusable Object-Oriented Software" by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides), laying the foundation for its widespread adoption in modern software development. It ensures that a class has only one instance while providing a global access point to that instance. This pattern is particularly useful for managing shared resources, such as database connections, logging mechanisms, and configuration settings.


Image description

Why Singleton?
The Singleton pattern is used in scenarios where:

  • Single Instance Requirement: A class must have exactly one instance, such as a configuration manager or logger.

  • Global Access: The instance needs to be globally accessible.

  • Resource Management: Shared resources like file systems, thread pools, or database connections benefit from controlled and centralized access.


Implementing Singleton in Java
There are several ways to implement the Singleton pattern in Java, each with its pros and cons. Below are some common approaches:

1. Eager Initialization
In this approach, the instance is created at the time of class loading. While simple and straightforward, it’s not ideal for cases where the instance might not be used, leading to wasted resources. For example, consider a Singleton managing a database connection pool in an application that may not always require database access during its lifecycle. Eager initialization would allocate resources unnecessarily in such scenarios, causing inefficiencies.

class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Lazy Initialization
Here, the instance is created only when it’s requested for the first time. This is efficient if the Singleton instance is not always needed. However, without thread safety, issues can arise when multiple threads access the getInstance method simultaneously, potentially leading to multiple instances being created, thus violating the Singleton principle.

class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Thread-Safe Singleton
To make the Lazy Initialization thread-safe, we use the synchronized keyword. This ensures that only one thread can access the initialization code at a time, though it can introduce performance bottlenecks.

class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Bill Pugh Singleton
This approach uses a static inner helper class to leverage the Java class loading mechanism for thread-safe lazy initialization without synchronization overhead.

class BillPughSingleton {
    private BillPughSingleton() {}

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

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
Enter fullscreen mode Exit fullscreen mode

Challenges with Singleton
Image description

Serialization
Serialization can create new instances of a Singleton during deserialization. When a Singleton object is serialized and then deserialized, the default behavior of Java can result in the creation of a new instance, violating the Singleton principle. To prevent this, override the readResolve() method. This method ensures that during deserialization, the existing Singleton instance is returned instead of creating a new one.

class SerializedSingleton implements java.io.Serializable {
    private static final SerializedSingleton INSTANCE = new SerializedSingleton();

    private SerializedSingleton() {}

    public static SerializedSingleton getInstance() {
        return INSTANCE;
    }

    protected Object readResolve() {
        return INSTANCE;
    }
}
Enter fullscreen mode Exit fullscreen mode

Cloning
Cloning can also break the Singleton pattern by creating a new instance. Override the clone() method to prevent this.

class CloneSafeSingleton implements Cloneable {
    private static final CloneSafeSingleton INSTANCE = new CloneSafeSingleton();

    private CloneSafeSingleton() {}

    public static CloneSafeSingleton getInstance() {
        return INSTANCE;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton, cannot be cloned");
    }
}
Enter fullscreen mode Exit fullscreen mode

Reflection
Reflection can bypass private constructors and create new instances by directly invoking the constructor, even if it's private. This can lead to the creation of multiple instances, breaking the Singleton pattern. To address this risk, include a check in the constructor to see if an instance already exists, and throw an exception if it does. This effectively prevents reflection from compromising the Singleton by enforcing the one-instance constraint at the constructor level.

class ReflectionSafeSingleton {
    private static final ReflectionSafeSingleton INSTANCE = new ReflectionSafeSingleton();

    private ReflectionSafeSingleton() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Instance already exists");
        }
    }

    public static ReflectionSafeSingleton getInstance() {
        return INSTANCE;
    }
}
Enter fullscreen mode Exit fullscreen mode

😀🚀✨Real-World Examples

Image description
Database Connection Manager

To implement a Singleton for managing MySQL database connections, follow these steps:

  • Create the Singleton Class:
    Define a DatabaseConnectionManager class with a private constructor to restrict instantiation.

  • Include a Static Instance:
    Add a static variable to hold the single instance of the class.

  • Implement Thread-Safe Initialization:
    Use the Bill Pugh Singleton or double-checked locking for thread safety.

  • Manage Database Connection Logic:
    Include methods to initialize, fetch, and close the MySQL database connection.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnectionManager {

    private static DatabaseConnectionManager instance;
    private Connection connection;
    private static final String URL = "jdbc:mysql://localhost:3306/yourDatabase";
    private static final String USER = "yourUsername";
    private static final String PASSWORD = "yourPassword";

    private DatabaseConnectionManager() {
        try {
            connection = DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException("Failed to create the database connection.");
        }
    }

    public static DatabaseConnectionManager getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionManager.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionManager();
                }
            }
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }

    public void closeConnection() {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage Example:

public class Main {
    public static void main(String[] args) {
        DatabaseConnectionManager dbManager = DatabaseConnectionManager.getInstance();
        Connection connection = dbManager.getConnection();

        try {
            // Perform database operations
            System.out.println("Database connected successfully.");
        } finally {
            dbManager.closeConnection();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Logger
A Singleton Logger class ensures that all parts of the application write logs to the same instance.

class Logger {
    private static Logger instance;

    private Logger() {}

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

    public void log(String message) {
        System.out.println("Log: " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion
The Singleton design pattern is a powerful tool for managing shared resources and ensuring controlled access to a single instance of a class. However, careful consideration must be given to serialization, cloning, reflection, and multi-threaded environments to maintain the integrity of the Singleton. By understanding and implementing the various approaches, you can effectively use the Singleton pattern in your Java applications.

Top comments (0)