Low Level Design
Singleton Pattern — Thread-Safe Implementations in Java
A deep dive into the Singleton design pattern: why it exists, common pitfalls, and three progressively better thread-safe implementations.
What is the Singleton Pattern?#
The Singleton is a creational design pattern that ensures a class has only one instance and provides a global access point to it.
You reach for Singleton when:
- A single shared resource must exist (database connection pool, config manager, logger)
- Creating multiple instances would cause incorrect behaviour or waste resources
Class Diagram#
Naive Implementation (Not Thread-Safe)#
public class Singleton {
private static Singleton instance;
private Singleton() {} // prevent external instantiation
public static Singleton getInstance() {
if (instance == null) { // ← race condition here in multi-threaded code
instance = new Singleton();
}
return instance;
}
public void doWork() {
System.out.println("Singleton doing work.");
}
}
Problem: Two threads can both see instance == null and create two separate objects.
Fix 1 — Synchronized Method#
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Trade-off: Correct but slow — every getInstance() call acquires a lock, even after initialisation.
Fix 2 — Double-Checked Locking (Recommended)#
public class Singleton {
// volatile ensures visibility of writes across threads
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // first check (no lock)
synchronized (Singleton.class) {
if (instance == null) { // second check (with lock)
instance = new Singleton();
}
}
}
return instance;
}
}
The volatile keyword prevents the JVM from reordering the write to instance before the constructor finishes, which is the subtle bug DCL had before Java 5.
Fix 3 — Initialization-on-Demand Holder (Cleanest)#
public class Singleton {
private Singleton() {}
// Inner class is loaded only on first access to getInstance()
private static final class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
This leverages the JVM's class-loading guarantee: a class is initialised exactly once, lazily, and in a thread-safe manner — no synchronisation overhead.
Extending the Design — Registry of Singletons#
When you need multiple named singletons (e.g., per-tenant configurations):
import java.util.concurrent.ConcurrentHashMap;
public class SingletonRegistry {
private static final ConcurrentHashMap<String, Singleton> registry =
new ConcurrentHashMap<>();
public static Singleton getInstance(String key) {
return registry.computeIfAbsent(key, Singleton::new);
}
public static class Singleton {
private final String name;
private Singleton(String name) {
this.name = name;
}
public String getName() { return name; }
}
}
computeIfAbsent is atomic — no race condition possible.
When NOT to Use Singleton#
| Situation | Why to avoid |
|---|---|
| Unit testing | Hidden global state makes test isolation hard |
| Dependency injection containers | The container already manages lifecycle |
| Stateful singletons in distributed systems | Each JVM gets its own instance — not truly single |
Summary#
| Implementation | Lazy | Thread-safe | Notes |
|---|---|---|---|
| Naive | ✅ | ❌ | Never use in prod |
| Synchronized method | ✅ | ✅ | Simple but slow |
| Double-checked locking | ✅ | ✅ | Requires volatile |
| Holder idiom | ✅ | ✅ | Cleanest, preferred |
Complete Implementation#
Here are all the files needed to build the Singleton Registry pattern. Browse each tab to see how the classes fit together:
public final class Singleton {
/** Lazy, thread-safe via class-loading guarantee. */
private static final class Holder {
static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
public void doWork() {
System.out.println("Working: " + this);
}
}import java.util.concurrent.ConcurrentHashMap;
/**
* Registry of named singletons — useful when you need
* more than one logically-singleton object (e.g. per-tenant).
*/
public class SingletonRegistry {
private static final ConcurrentHashMap<String, Singleton> registry =
new ConcurrentHashMap<>();
private SingletonRegistry() {}
/** Returns the singleton for the given key, creating it if absent. */
public static Singleton getInstance(String key) {
// computeIfAbsent is atomic — no double-creation race possible
return registry.computeIfAbsent(key, k -> new Singleton());
}
}public class Main {
public static void main(String[] args) {
// Direct singleton — always the same object
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("Same instance: " + (s1 == s2)); // true
// Registry — one per key
Singleton tenantA1 = SingletonRegistry.getInstance("tenantA");
Singleton tenantA2 = SingletonRegistry.getInstance("tenantA");
Singleton tenantB = SingletonRegistry.getInstance("tenantB");
System.out.println("A1 == A2: " + (tenantA1 == tenantA2)); // true
System.out.println("A1 == B: " + (tenantA1 == tenantB)); // false
}
}