How to Implement Thread-Safe Singleton?
If two threads simultaneously check instance == null, both will create their own object — resulting in two instances instead of one. To make Singleton work in a multithreaded en...
Junior Level
If two threads simultaneously check instance == null, both will create their own object — resulting in two instances instead of one.
To make Singleton work in a multithreaded environment, you need to protect instance creation.
The simple problem:
// Two threads can simultaneously create two instances!
if (instance == null) {
instance = new Singleton(); // Thread 1 and Thread 2 at the same time!
}
The simplest approach — Enum:
public enum Singleton {
INSTANCE; // JVM guarantees a single instance
public void doWork() {
System.out.println("Working...");
}
}
// Usage
Singleton.INSTANCE.doWork();
// Thread-safe, protected from reflection and serialization
Why Enum is safe:
- Java forbids creating enums via
new - Serialization works correctly
- Concurrency is provided by JVM
When NOT to implement thread-safe Singleton manually
In Spring applications: @Component + singleton scope (default) already manages this.
In CDI: @ApplicationScoped. Don’t reinvent the wheel.
Middle Level
Approach 1: Enum Singleton (Recommended)
public enum Singleton {
INSTANCE;
private final String config;
Singleton() {
this.config = loadConfig();
}
public String getConfig() {
return config;
}
}
Advantages:
- Thread-safe (JVM)
- Protected from reflection
- Protected from serialization
- Simple code
Disadvantages:
- Not lazy (created on class loading)
Approach 2: Bill Pugh (Static Holder)
public class BillPughSingleton {
private BillPughSingleton() {}
// Inner class loads only on first access
private static class SingletonHolder {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
// Serialization protection
protected Object readResolve() {
return getInstance();
}
}
Why it works:
- JVM guarantees atomicity of static field initialization
SingletonHolderclass loads only on firstgetInstance()call- Lazy initialization without explicit synchronized — thread-safety ensured by ClassLoader locking (JLS 12.4.2).
Approach 3: Double-Checked Locking
public class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {
if (instance != null) {
throw new IllegalStateException("Already created");
}
}
public static DclSingleton getInstance() {
DclSingleton local = instance; // Optimization
if (local == null) {
synchronized (DclSingleton.class) {
local = instance;
if (local == null) {
instance = local = new DclSingleton();
}
}
}
return local;
}
}
Why volatile is needed:
- Without volatile, reordering is possible
- A thread may get a reference to an UNINITIALIZED object
- volatile prevents reordering
Before Java 5 (JSR-133), the Memory Model did not guarantee happens-before for volatile, and DCL was broken.
Comparison
| Approach | Lazy | Thread-Safety | Complexity |
|---|---|---|---|
| Enum | No | Full | Minimal |
| Bill Pugh | Yes | Full | Low |
| DCL + volatile | Yes | Full | High |
In Spring
// NO need to write Singleton manually!
@Component // Singleton scope by default
public class UserService { }
// Spring itself guarantees a single instance
// + testability through DI
Senior Level
Java Memory Model Deep Dive
Problem without volatile:
// instance = new Singleton() consists of 3 steps:
// 1. Allocate memory
// 2. Invoke constructor
// 3. Assign reference to instance
// Without volatile, reorder is possible: 1 -> 3 -> 2
// Thread B sees instance != null at step 3
// But constructor (step 2) is not yet executed!
// -> Thread B gets an object with default fields!
Volatile solves it:
// volatile inserts Memory Barriers:
// StoreStore before writing to volatile
// LoadLoad after reading from volatile
// This guarantees:
// All writes in constructor are completed BEFORE reference is published
Local Variable Optimization:
public static DclSingleton getInstance() {
DclSingleton local = instance; // Read volatile ONCE
if (local == null) {
synchronized (DclSingleton.class) {
local = instance;
if (local == null) {
instance = local = new DclSingleton();
}
}
}
return local; // Use local variable
}
// Without optimization: 2 volatile reads (slow)
// With optimization: 1 volatile read (~25% faster)
VarHandle (Java 9+)
// For extreme performance
public class VarHandleSingleton {
private static final VarHandle HANDLE = MethodHandles.lookup()
.findStaticVarHandle(VarHandleSingleton.class, "instance", VarHandleSingleton.class);
private static VarHandleSingleton instance;
public static VarHandleSingleton getInstance() {
VarHandleSingleton local = (VarHandleSingleton) HANDLE.getAcquire();
if (local == null) {
synchronized (VarHandleSingleton.class) {
local = (VarHandleSingleton) HANDLE.getAcquire();
if (local == null) {
local = new VarHandleSingleton();
HANDLE.setRelease(null, local);
}
}
}
return local;
}
}
// getAcquire/setRelease are cheaper than volatile
// But sufficient for DCL semantics
Initialization-on-demand Holder: Internal Mechanics
// JVM Class Loading mechanism:
// 1. Class loads on first access
// 2. Static field initialization = atomic
// 3. ClassLoader internal locking guarantees thread-safety
// This is NOT "magic" — it's JLS (Java Language Specification) 12.4.1:
// "Initialization of a class consists of executing its static initializers"
// "The Java programming language implements multiple-threaded initialization correctly"
Protection from All Attacks
public class SecureSingleton implements Serializable {
private static final SecureSingleton INSTANCE = new SecureSingleton();
private SecureSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Singleton violation");
}
}
public static SecureSingleton getInstance() {
return INSTANCE;
}
// Serialization protection
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
// Cloning protection
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Singleton cannot be cloned");
}
}
Performance Comparison
Benchmark (10M calls):
Enum: 10ms (static final, JIT optimizes)
Bill Pugh: 12ms (class loading once)
DCL + volatile: 15ms (volatile read overhead)
VarHandle: 11ms (weaker barriers)
synchronized: 150ms (lock overhead every time!)
Production Experience
Real scenario: DCL without volatile in production
- Application worked for 2 years without issues
- After migrating to new JDK → random NPEs
- Cause: JDK changed JIT optimization strategy
- Solution: added volatile → problem disappeared
Best Practices
- Enum — for 99% of cases (safety > laziness)
- Bill Pugh — when laziness is critical
- DI Container — in Spring applications (DON’T write manually!)
- volatile is MANDATORY in DCL
- Local variable optimization in DCL
- readResolve() for serialization protection
- Constructor check for reflection protection
- VarHandle (Java 9+) for extreme performance
Senior Summary
- JMM: volatile prevents reordering through Memory Barriers
- Bill Pugh: relies on JLS 12.4.1 guarantees
- Enum: native protection from reflection/serialization
- Local variable: reduces volatile reads by 25%
- VarHandle: weaker barriers -> faster than volatile
- Spring: DON’T write Singleton — use
@Component - DCL without volatile = a ticking time bomb
Interview Cheat Sheet
Must know:
- Enum Singleton — recommended approach: thread-safe, protected from reflection and serialization
- Bill Pugh (Static Holder) — lazy initialization without synchronized, via static inner class (JLS 12.4.1)
- Double-Checked Locking requires volatile to prevent instruction reordering
- Local variable optimization in DCL reduces volatile reads by 25%
- In Spring applications DON’T write Singleton manually — @Component with singleton scope
- VarHandle (Java 9+) provides getAcquire/setRelease — cheaper than volatile
- Without volatile: reordering 1->3->2 (memory -> publish -> constructor) -> uninitialized object
Common follow-up questions:
- Why is volatile mandatory in DCL? — Without it, JIT/CPU may reorder: allocate memory -> publish reference -> call constructor
- How does Bill Pugh work? — Nested class loads only on first access, JLS guarantees atomic initialization
- Enum vs Bill Pugh — which to choose? — Enum if laziness is not needed, Bill Pugh if laziness is critical
- Why doesn’t Spring require manual Singleton? — @Component with singleton scope, + testability through DI
Red flags (DO NOT say):
- “In Spring I write Singleton manually via synchronized” — @Component already manages this
- “Volatile is not needed, it works for me” — reordering appears only on specific CPU/JDK
- “DCL without volatile is safe” — undefined behavior, especially on ARM processors
- “Enum doesn’t fit Singleton” — this is the Joshua Bloch recommended approach
Related topics:
- [[03. What is Singleton]] — general pattern description
- [[05. What is double-checked locking]] — DCL details
- [[06. What are problems with Singleton]] — Singleton problems
- [[02. What pattern categories exist]] — Creational patterns
- [[16. What anti-patterns do you know]] — when Singleton becomes an anti-pattern