Mastering the Singleton Pattern in Java

I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.
The Singleton pattern ensures that a class has only one instance and provides a way to access it from anywhere in the code.
✅ When to use it:
- You need a single, shared object — like an app-wide settings manager, log writer, or clock.
❌ When not to use it:
- You need multiple independent instances (e.g., one per user or request).
🔤 Basic Implementation (Eager, Thread-Safe)
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
Key points:
A private constructor to prevent the creation of additional instances
A single private instance that is made accessible in the getInstance method
The instance is initialized when the class is loaded, thus, it is eager
🔤 Basic Implementation (Lazy, Not Thread-Safe)
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
This lazy implementation instantiates the instance when the getInstance is invoked for the first time, thus it is lazy. This looks fine at first glance, but it fails in multi-threaded environments.
In a multi-threaded program, you can’t control how threads are scheduled. Two threads might call getInstance() at the same time, both see that instance is still null, and both proceed to create a new object. This results in two different instances, breaking the singleton rule.
This issue is called a race condition — and we’ll now look at thread-safe alternatives that avoid it.
Real-World Examples of the Singleton Pattern
Many frameworks use the Singleton pattern to manage shared components.
📦 Spring Framework
By default, Spring creates beans as singletons. When you annotate a class with @Component, Spring instantiates the class once and reuses it throughout the application:
javaCopyEdit@Component
public class MyService {
// Singleton bean by default
}
🧪 JUnit
JUnit uses a singleton test runner internally to manage and coordinate test execution. This ensures all tests run in a consistent and isolated environment.
Thread-Safe Singleton Implementations
Here are five common ways to implement a thread-safe singleton in Java — each with a short explanation, code sample, and its key advantage.
🔐 1. Eager Initialization (Simple, Thread-Safe)
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
✅ Advantage: Very simple and thread-safe because the instance is created when the class loads.
❌ Downside: The instance is created even if it's never used.
🧵 2. Synchronized Method (Safe but Slow)
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
✅ Advantage: Easy to write and understand.
❌ Downside: Synchronization happens on every call, which can hurt performance.
🌀 3. Double-Checked Locking (Efficient & Lazy)
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;
}
}
✅ Advantage: Thread-safe, lazy, and avoids unnecessary locking.
⚠️ Downside: Slightly more complex to write and understand.
📥 4. Static Inner Class (Elegant & Lazy)
public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class Holder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
✅ Advantage: Thread-safe, lazy, and no synchronization overhead.
❌ Downside: Still vulnerable to reflection and serialization attacks.
🛡️ 5. Enum Singleton (Most Robust)
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// ...
}
}
✅ Advantage: Thread-safe, lazy, and fully protected against reflection and serialization attacks.
🏆 This is the safest and most recommended approach, especially in libraries or critical systems.
📘 Recommended in Effective Java by Joshua Bloch.
Summary: Best Thread-Safe Singleton Approaches
| Approach | Lazy? | Robustness | Performance |
| 🏆 Enum Singleton | ✅ | ✅ Fully protected | ✅ Excellent |
| Static Inner Class | ✅ | ❌ Vulnerable to reflection | ✅ Excellent |
| Double-Checked Locking | ✅ | ❌ Vulnerable to reflection | ✅ Good |
| Eager Initialization | ❌ | ❌ Vulnerable to reflection | ✅ Excellent |
| Synchronized Method | ✅ | ❌ Vulnerable to reflection | ❌ Slower |
⚠️ Disadvantages of the Singleton Pattern
While the Singleton pattern can be useful, it comes with several important disadvantages, especially when overused or misunderstood:
Global state: Acts like a global variable, making code harder to manage and test.
Hard to test: Difficult to mock or replace in unit tests.
Breaks SOLID principles: Often violates Single Responsibility and Dependency Inversion.
Concurrency issues: Poor implementation can create multiple instances in multi-threaded apps.
Long-lived lifecycle: Can lead to memory leaks or outdated state.
Tight coupling: Makes refactoring or replacing the singleton harder over time.
Conclusion
The Singleton pattern is a simple and effective tool for ensuring that a class has only one instance and a global point of access. It’s useful for managing shared resources like configuration, logging, or system-wide coordination.
Understanding the trade-offs between different implementations will help you choose the safest and most efficient approach for your needs — and avoid common pitfalls along the way.
We recommend the enum-based singleton for its simplicity and robustness. It’s inherently thread-safe, lazy, and protected against reflection and serialization attacks, making it the safest choice in most scenarios. It’s also the approach recommended by Effective Java author Joshua Bloch.




