Singleton Pattern: Thread-Safe Implementations and When to Avoid It
Every thread-safe Singleton implementation in Java — double-checked locking, the static holder idiom, and enum singleton — plus why the pattern is the most abused tool in OOP and when dependency injection should replace it.
Singleton Pattern
Singleton ensures a class has exactly one instance and provides a single, well-known access point to it. It is also the most misused pattern in the industry — reached for by default whenever a developer wants "one of something," even when that something is exactly the kind of shared mutable state that makes systems hard to test and reason about. This guide covers every correct thread-safe implementation, the ways each one breaks, and — more importantly — how to recognize when Singleton is the right tool versus when it is global state wearing a design-pattern costume.
1. Intent
Some things in a system genuinely have one instance by nature of the problem: one connection pool per process, one in-memory config cache reflecting the process's environment, one hardware resource handle. Singleton's job is to enforce that cardinality at the type level — make it structurally impossible to accidentally construct a second one — rather than trusting every caller to remember "don't new this, use the shared one."
The pattern has two obligations, and most broken implementations satisfy only one:
- Exactly one instance exists for the lifetime of the process (or a well-defined scope).
- Global access to that instance is provided without passing a reference around explicitly.
Obligation 2 is precisely what makes Singleton controversial: global access is global coupling.
2. The Naive Approach (Violation)
// VIOLATION: not thread-safe — two threads can both see instance == null
// and both construct a ConfigManager, defeating the "exactly one" guarantee.
public class ConfigManager {
private static ConfigManager instance;
private final Map<String, String> settings = new HashMap<>();
private ConfigManager() {
// expensive: loads settings from disk/network
settings.put("env", "production");
}
public static ConfigManager getInstance() {
if (instance == null) { // Thread A and Thread B can both pass this check
instance = new ConfigManager(); // before either assigns — TWO instances created
}
return instance;
}
}Under concurrent first access, two threads can race between the null check and the assignment. Both construct a ConfigManager; whichever assignment happens last "wins," and the other instance is silently discarded after doing its (possibly expensive, possibly side-effecting) construction work. In the best case this wastes work. In the worst case — if the constructor opens a file handle, registers a listener, or increments a counter — it corrupts state permanently.
3. Structure
The structure is intentionally minimal: a private constructor (blocking new from outside), a private static field holding the sole instance, and a public static accessor. Every implementation below is a variation on how that accessor guarantees thread safety and laziness.
4. Thread-Safe Implementations
4.1 Eager Initialization
public class EagerConfigManager {
// Instance created at class-loading time — JVM guarantees this is thread-safe
// because static initializers run under an implicit class-init lock.
private static final EagerConfigManager INSTANCE = new EagerConfigManager();
private EagerConfigManager() { /* load settings */ }
public static EagerConfigManager getInstance() {
return INSTANCE;
}
}Simplest possible correct Singleton. The trade-off: the instance is created the moment the class is loaded, whether or not it's ever used — wasteful if construction is expensive and the class might not always be needed on a given code path.
4.2 Synchronized Method (Correct but Slow)
public class SynchronizedConfigManager {
private static SynchronizedConfigManager instance;
private SynchronizedConfigManager() { }
// Correct: only one thread can execute this method at a time.
// Wrong trade-off: EVERY call pays lock-acquisition cost, forever,
// even though the race only matters on the very first call.
public static synchronized SynchronizedConfigManager getInstance() {
if (instance == null) {
instance = new SynchronizedConfigManager();
}
return instance;
}
}Synchronizing the entire accessor is correct but pessimistic: it serializes every call for the entire life of the program, when the only unsafe window is the first, uninitialized call. On a hot path called millions of times, this is measurable overhead for a race condition that only exists once.
4.3 Double-Checked Locking with volatile
public class DclConfigManager {
// volatile is NOT optional: without it, another thread can observe a
// partially-constructed object due to instruction reordering.
private static volatile DclConfigManager instance;
private DclConfigManager() { }
public static DclConfigManager getInstance() {
DclConfigManager result = instance;
if (result == null) { // 1st check: no lock, fast path
synchronized (DclConfigManager.class) {
result = instance;
if (result == null) { // 2nd check: inside the lock
instance = result = new DclConfigManager();
}
}
}
return result;
}
}Without volatile, the JVM/JIT is legally allowed to reorder the write to instance so the reference is visible to another thread before the constructor has finished running on it — a thread can then read a non-null but half-initialized object. This is not theoretical; it was the reason the original (pre-Java 5) double-checked locking idiom was declared broken. volatile establishes a happens-before edge that prevents the reorder.
Why the local variable result? It's a minor but real optimization: without it, instance (a volatile field) would be read from main memory up to three times per call on the fast path; caching it in a local reduces that to one read in the common case.
4.4 Static Inner Class Holder (Initialization-on-Demand Holder)
public class HolderConfigManager {
private HolderConfigManager() { }
// The JVM does not load/initialize a nested static class until it's
// first referenced. That deferred, one-time class-init is *itself*
// thread-safe per the JLS — no explicit locking needed at all.
private static class Holder {
static final HolderConfigManager INSTANCE = new HolderConfigManager();
}
public static HolderConfigManager getInstance() {
return Holder.INSTANCE;
}
}This is the cleanest hand-rolled option: lazy (the Holder class, and therefore the instance, is only loaded on first call to getInstance()), thread-safe (guaranteed by JVM class-loading semantics, not by code you have to get right), and has zero synchronization overhead after the first call — there isn't even a volatile read, unlike double-checked locking.
4.5 Enum Singleton (Joshua Bloch's Recommendation)
public enum EnumConfigManager {
INSTANCE;
private final Map<String, String> settings = new HashMap<>();
// Enum constructors run exactly once, guaranteed by the JVM, and are
// inherently thread-safe — same guarantee as static initializers.
EnumConfigManager() {
settings.put("env", "production");
}
public String get(String key) {
return settings.get(key);
}
}
// Usage:
String env = EnumConfigManager.INSTANCE.get("env");Effective Java (Item 3) recommends this as the best way to implement a Singleton in Java, for a reason that isn't obvious until you've been burned by it: it's the only implementation that's safe against both reflection and serialization attacks without extra code.
5. Breaking Singleton via Reflection (and Why Enum Survives It)
// Reflection can call a "private" constructor directly, bypassing the
// entire point of the pattern:
Constructor<HolderConfigManager> ctor =
HolderConfigManager.class.getDeclaredConstructor();
ctor.setAccessible(true);
HolderConfigManager second = ctor.newInstance(); // a SECOND instance, legally created
System.out.println(second == HolderConfigManager.getInstance()); // false — broken!Every class-based implementation above (eager, synchronized, DCL, holder) is vulnerable to this: setAccessible(true) strips the private modifier's enforcement, and newInstance() happily runs the constructor again.
// Enum resists reflection — the JVM itself throws before your code even runs:
Constructor<EnumConfigManager> ctor =
EnumConfigManager.class.getDeclaredConstructor();
ctor.setAccessible(true);
EnumConfigManager second = ctor.newInstance();
// throws java.lang.IllegalArgumentException: Cannot reflectively create enum objectsThe JVM special-cases enum instantiation at the reflection API level — this protection is baked into Constructor.newInstance() itself, not something you coded.
Serialization has the same asymmetry: a class-based Singleton that implements Serializable will, by default, produce a new instance on deserialization unless you manually add readResolve():
// Without this, deserializing a serialized Singleton creates a second instance:
protected Object readResolve() {
return Holder.INSTANCE; // force deserialization to return the existing instance
}Enums serialize by name and are deserialized by looking up the existing constant — readResolve() is unnecessary because the JVM's enum deserialization mechanism already guarantees singularity.
The enum singleton isn't a trick — it's using a language feature (enums are guaranteed-single instances of their constants) that happens to solve the exact problem Singleton is trying to hand-roll. The trade-off is stylistic: it can't extend a base class (enums implicitly extend Enum), and lazy initialization control is coarser.
6. Why Singletons Kill Testability
// PaymentService reaches out to a global, unmockable dependency
class PaymentService {
void charge(Order order) {
AuditLogger.getInstance().log("charging order " + order.getId()); // static global access
// ... charge logic
}
}
// Testing charge() now ALSO exercises AuditLogger's real singleton state.
// You cannot substitute a test double without changing PaymentService's code,
// because getInstance() is a static call baked directly into the method body.This is the practical reason experienced engineers treat Singleton with suspicion: a class that reaches for Singleton.getInstance() internally has a hidden dependency. It doesn't show up in the constructor signature, so:
- You can't inject a fake/mock for the test — the dependency is resolved by a static call, not passed in.
- Tests that run in the same JVM process share the Singleton's state, so test order can affect results (a classic flaky-test source).
- Parallel test execution becomes unsafe if the Singleton is mutable.
This is precisely the seam that Dependency Inversion addresses: depend on an injected abstraction, not a concrete global. The fix is almost always the same shape:
// FIXED: the "one instance" guarantee is now the CALLER'S responsibility
// (typically a DI container), not baked into the class itself.
interface AuditLogger {
void log(String message);
}
class PaymentService {
private final AuditLogger auditLogger;
PaymentService(AuditLogger auditLogger) { // injected — swappable in tests
this.auditLogger = auditLogger;
}
void charge(Order order) {
auditLogger.log("charging order " + order.getId());
}
}
// Production: one AuditLogger instance is created once and wired everywhere
// (a Spring @Bean is a singleton by default scope) — same "one instance" property,
// none of the static-access coupling.A Spring @Bean (default scope) is a Singleton in the strict sense — one instance per application context. The difference from the classic GoF pattern is where the "exactly one" guarantee lives: in hand-rolled Singleton it's enforced inside the class via a static field; in DI it's enforced by the container, and the class itself stays an ordinary, testable, constructor-injected class with no static state at all. Prefer the container-managed version whenever one is available.
7. When Singleton Is Justified vs. When It's an Anti-Pattern
| Use Singleton when | Avoid it when |
|---|---|
| The resource is genuinely process-wide and stateless or read-only (e.g. an immutable config snapshot loaded once) | The "singleton" holds mutable state that different tests or requests need to vary independently |
| You're managing a scarce, expensive-to-create shared resource (thread pool, DB connection pool) where a second instance would exhaust resources or double-connect | You reach for it because it's "convenient to call from anywhere" rather than because two instances would be incorrect |
No DI container is available (e.g. a lightweight library, a java.util.logging-style facility) | You're in a framework (Spring, etc.) that already manages singleton-scoped beans — use that instead of a hand-rolled static |
The type is a hardware/OS resource handle where the OS itself only exposes one (e.g. a Runtime object) | The class needs to be unit tested in isolation with different configurations per test |
The failure mode isn't the pattern itself — it's using a class-level static field as a smuggling mechanism for global mutable state, then justifying it after the fact with "well, it's a Singleton." If you'd be uncomfortable making the same field a public static mutable variable directly, wrapping it in getInstance() doesn't fix the underlying problem.
8. Singleton vs. Monostate
Singleton and Monostate solve the same underlying problem — "this behavior should act like there's only one of it" — with an inverted trade-off:
| Singleton | Monostate | |
|---|---|---|
| Instances | Exactly one object ever exists | Any number of objects can be constructed |
| State location | Instance fields, but only one instance | static fields, shared across all instances |
| Access pattern | Singleton.getInstance().method() — callers know it's special | new Monostate().method() — callers use it like an ordinary object |
| Subclassing | Awkward — the "one instance" guarantee usually lives in a final class or a private constructor | Natural — subclasses inherit the shared static state transparently |
| Polymorphism | Limited (can't easily have multiple kinds of the one instance) | Full polymorphism available since it's ordinary object construction |
If client code shouldn't have to know or care that it's dealing with a shared-state object, Monostate is often the better-hidden version of the same idea. See the Monostate Pattern guide for the full implementation and trade-offs.
9. Real-World / Production Examples
java.lang.Runtime.getRuntime()— one JVM, one runtime handle; a textbook justified case since the OS process itself is singular.- Logging frameworks (Log4j2's
LoggerContext, SLF4J's binding) — logger factories are effectively singletons per classloader. - Spring's default bean scope — every
@Component/@Service/@Beanis singleton-scoped within theApplicationContextby default; this is Singleton's benefit (one shared instance) without the static-access coupling cost. - Database connection pools (HikariCP's
HikariDataSource) — one pool per configured datasource; having two independent pools against the same DB with the same config would just fragment the connection budget. java.awt.Desktop.getDesktop()— one desktop integration handle per JVM, mirroring a genuine OS-level singular resource.
Interview Questions
- Walk through why the naive
if (instance == null) instance = new X();is broken under concurrency, including what "broken" actually means for the caller. - Why is
volatilemandatory for double-checked locking to be correct, not just a performance nicety? - Explain the static inner class holder idiom. Why is it thread-safe without any explicit
synchronizedblock? - Why does Joshua Bloch recommend enum singletons? What two specific attack vectors does it close that a class-based singleton doesn't?
- How would you break a class-based Singleton using reflection, and what does
readResolve()protect against? - Why do Singletons make unit testing harder? What's the concrete failure mode (not just "it's bad practice")?
- How does a Spring
@Servicebean give you Singleton's benefit without its testability cost? - Give an example of a legitimate use of Singleton and one where it's clearly an anti-pattern, and justify the difference.