Singleton Pattern in Java: Beyond the Basics
“There should only ever be one.” — That sounds like the plot of a dystopian movie. But in software engineering, that’s the Singleton Pattern, and it plays a critical role in system design. And no, it’s not just a glorified global variable.
In this blog, we’ll go from a naive implementation to the most production-grade patterns of Singleton in Java. We’ll uncover the why, the how, and the why not of each variant, comparing them on safety, performance, and elegance.
📖 Not a Medium member? You can read this article for free using this friend link: Singleton Pattern in Java: Beyond the Basics
🔥 What is the Singleton Pattern?
The Singleton Pattern ensures that a class has only one instance in the JVM and provides a global access point to it.
Common Use Cases:
- Configuration classes (e.g., AppConfig)
- Logging services
- Caching mechanisms
- Thread pools
- Database connection pools
Singleton is useful when:
- You want a shared resource across the app
- You want to control access to a single point of truth
🧱 The Naive Singleton (Thread-Unsafe)
public class AppConfig {
private static AppConfig instance;
private AppConfig() {}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
}
❌ Problem:
- This implementation is not thread-safe. If multiple threads call getInstance() at the same time, it can create multiple instances.
- This breaks the singleton guarantee.
🔐 Synchronized Method (Thread-Safe but Slower)
public class AppConfig {
private static AppConfig instance;
private AppConfig() {}
public static synchronized AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
}
✅ Thread-safe
❌ Performance hit:
- Every call to getInstance() acquires a lock, even after the instance is initialized.
Use only in low-concurrency scenarios or educational demos.
⚡ Double-Checked Locking (with volatile)
public class AppConfig {
private static volatile AppConfig instance;
private AppConfig() {}
public static AppConfig getInstance() {
if (instance == null) {
synchronized (AppConfig.class) {
if (instance == null) {
instance = new AppConfig();
}
}
}
return instance;
}
}
✅ Thread-safe
✅ Lazy-loaded
✅ High-performance
Why volatile?
Without volatile, the JVM might reorder instructions due to optimizations. This could result in a thread seeing a partially constructed object.
By marking the instance as volatile, you prevent that reordering and guarantee full object construction visibility across threads.
💡 Static Holder Class (Bill Pugh Singleton)
public class AppConfig {
private AppConfig() {}
private static class Holder {
private static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
}
✅ Thread-safe
✅ Lazy-loaded
✅ No synchronized, no volatile
JVM Classloading Guarantees:
- Holder class is not loaded until getInstance() is called.
- JVM ensures thread-safe loading of classes.
This is arguably the cleanest and most efficient Singleton pattern in Java.
🚀 Eager Initialization (Anti-pattern in Some Cases)
public class AppConfig {
private static final AppConfig instance = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() {
return instance;
}
}
✅ Thread-safe
❌ Not lazy-loaded
- Memory is consumed even if the instance is never used.
- Useful only if you are 100% sure the singleton will be used.
🧠 When to Use Which Singleton?
Let’s simplify the decision-making:
Naive (non-thread-safe)
- Not safe in multi-threaded environments.
- Avoid in real applications.
Synchronized Method
- Thread-safe, simple to write.
- Performance suffers due to locking.
- Use in low-concurrency scenarios or quick prototypes.
Double-Checked Locking (with volatile)
- Lazy, fast, thread-safe.
- More complex but suitable for performance-critical apps.
Static Holder Class
- Best blend of simplicity and performance.
- Leverages JVM guarantees.
- Recommended for most real-world use cases.
Eager Initialization
- Use if instance is guaranteed to be needed.
- No overhead of lazy-loading.
🤔 Reflect & Apply — Questions for You:
If you can answer these, you’ve truly understood the Singleton pattern:
- What happens if volatile is removed from a double-checked locking Singleton?
- Why do we check if (instance == null) twice in that pattern?
- How does the JVM guarantee thread-safety with the static holder pattern?
- What are the dangers of using Singleton recklessly in large systems?
- Which Singleton variant would you use for a configuration loader that may or may not be used at runtime?
Take a few minutes. Think through them. If you’re unsure — scroll back up and re-read the explanations.
🫰 Final Thoughts
The Singleton pattern is deceptively simple. Many engineers fall into the trap of using it naively, turning their systems into a multithreaded mess.
In modern Java, your best bets are:
- Static Holder for clean, lazy, thread-safe instantiation
- Double-Checked Locking when you need explicit low-level control
Avoid eager singletons unless you must load it upfront. And never use non-thread-safe variants in real-world code.
📌 Bonus: Singleton is Not Always a Good Idea
Singletons are global state.
Overusing them:
- Destroys testability
- Introduces hidden dependencies
- Encourages tight coupling
Use them only when a single shared instance is truly necessary.
📉 Enjoyed This?
If this helped you level up your design pattern game:
🖐 Clap a few times to show support
💬 Drop a comment: Was this clear? Did you learn something new?
👥 Follow me for upcoming deep-dives on:
- Strategy Pattern — Dependency Injection’s Secret Weapon
- Observer Pattern — Event-Driven Systems Done Right
- Factory & Abstract Factory — Code That Builds Code
Let’s master software design — one pattern at a time.
Happy coding! ✨
Singleton Pattern in Java: Beyond the Basics was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More