Singleton Design Pattern in Java
Introduction
The Singleton Pattern is one of the simplest but most widely used creational design patterns. Its primary purpose is to ensure that a class has only one instance while providing a global point of access to that instance.
In real-world terms:
- Imagine a President of a country or a Central Bank. There can be only one at a time, and everyone refers to that single entity.
- Similarly, in software, some classes must have exactly one instance — for example, a configuration manager, logging service, or database connection pool.
Problem Statement
Consider a logging class:
public class Logger {
public void log(String message) {
System.out.println(message);
}
}
If every class in your application creates its own Logger object:
Logger logger1 = new Logger();
Logger logger2 = new Logger();
Problems arise:
- Multiple instances exist — consuming unnecessary memory.
- Inconsistent state — configuration or log files could be different for each instance.
- Difficult coordination — you may want a single centralized logger for uniform logging.
The Singleton Pattern solves this by ensuring only one instance exists throughout the application.
Implementation in Java
1. Eager Initialization
The simplest approach is to create the instance when the class loads.
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
// Private constructor prevents external instantiation
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
public void showMessage() {
System.out.println("Hello from EagerSingleton!");
}
}
// Usage
public class Main {
public static void main(String[] args) {
EagerSingleton singleton = EagerSingleton.getInstance();
singleton.showMessage();
}
}
Advantages:
- Simple and thread-safe.
- Instance is ready to use when needed.
Disadvantages:
- Object is created even if it’s never used (wasteful for heavy resources).
2. Lazy Initialization
The instance is created only when it’s first requested.
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from LazySingleton!");
}
}
Advantages:
- Saves resources by creating the instance only when needed.
Disadvantages:
- Not thread-safe in multi-threaded environments. Multiple threads could create multiple instances simultaneously.
3. Thread-Safe Singleton (Synchronized)
To handle multi-threaded scenarios:
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from ThreadSafeSingleton!");
}
}
Advantages:
- Safe for concurrent access.
Disadvantages:
- synchronized adds performance overhead every time getInstance() is called.
4. Double-Checked Locking (Optimized Thread-Safe)
Combines lazy initialization with thread safety without heavy overhead.
public class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
public void showMessage() {
System.out.println("Hello from DoubleCheckedSingleton!");
}
}
Key Point: volatile ensures visibility of changes across threads.
Advantages:
- Thread-safe.
- Efficient performance after first initialization.
5. Bill Pugh Singleton (Using Inner Static Class)
A modern, clean approach without synchronization overhead:
public class BillPughSingleton {
private BillPughSingleton() {}
// Inner static class holds the singleton instance
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
public void showMessage() {
System.out.println("Hello from BillPughSingleton!");
}
}
Advantages:
- Thread-safe without synchronized.
- Lazy-loaded only when getInstance() is called.
Real-World Analogy
- Database Connection Pool: You want only one pool managing all connections.
- Configuration Manager: One centralized object holds all app settings.
- Logging Service: One instance handles all logging across modules.
Advantages of Singleton Pattern
- Controlled Access: Only one instance exists.
- Memory Efficiency: Reduces overhead by reusing a single object.
- Global Access: Can be accessed from anywhere in the program.
- Thread Safety: With proper implementation, ensures safe multi-threaded access.
- Consistency: Shared state across the application.
Disadvantages
- Testing Difficulty: Singletons are hard to mock in unit tests.
- Hidden Dependencies: Overuse can lead to tightly coupled code.
- Global State: Can become similar to global variables if misused.
When to Use
- When exactly one instance of a class is required.
- When the instance should be globally accessible.
- For resources that are expensive to create (database connections, configuration objects, logging services).
Conclusion
The Singleton Pattern is a cornerstone of creational design patterns. It provides a single point of access and ensures consistency, memory efficiency, and controlled object creation.
In Java, there are multiple ways to implement a Singleton — Eager Initialization, Lazy Initialization, Thread-Safe, Double-Checked Locking, and Bill Pugh Inner Static Class. The choice depends on resource needs, performance, and thread safety requirements.
Next, we can explore the Prototype Design Pattern, which allows creating object copies instead of instantiating new ones, saving resources and improving performance.