LESSON 9.5
Thread-safe singleton and double-checked locking
Safely publish lazy singletons in multi-threaded runtimes without synchronization bottlenecks: double-checked locking, volatile memory barriers, and the Bill Pugh holder.
6 min read
Lazy initialization defers expensive object creation until the first time it is accessed. In multi-threaded environments like the JVM, if two threads call getInstance() simultaneously on an uninitialized singleton, both may pass the if (instance == null) check and instantiate two distinct instances, violating the singleton invariant.
Synchronizing the entire getInstance() method fixes the bug, but introduces a heavy synchronization bottleneck on every subsequent read for the entire lifetime of the application.
The problem
// Broken: race condition causes duplicate instances
public static Database getInstance() {
if (instance == null) {
// Thread A and Thread B both reach this line together!
instance = new Database(); // Created twice!
}
return instance;
}
// Inefficient: synchronization on every single read forever
public static synchronized Database getInstance() {
if (instance == null) instance = new Database();
return instance; // 99.999% of calls pay mutex penalty unnecessarily!
}The pattern
1. Double-Checked Locking (DCL)
Check if the instance is null. If null, acquire the lock and check again. The volatile modifier is mandatory to prevent CPU instruction reordering.
2. Bill Pugh Initialization-on-demand Holder
Use a private static nested class. The JVM guarantees that the inner class is not loaded until referenced by getInstance(), providing lazy, thread-safe publication with zero lock overhead.
In code
When to use it, and when not to
Use Double-Checked Locking or the Holder Idiom when:
- Creating the singleton resource is expensive in CPU or memory, and might never be used during a given program execution.
- You are running in a multi-threaded runtime (Java, C++, C#) where multiple threads access
getInstance()concurrently.
Don't use it when:
- Eager initialization (
private static final Instance instance = new Instance()) is fast and lightweight: eager creation at application startup is simpler and completely safe. - In JavaScript/Node.js: JS has no thread concurrency over shared module variables.
Where you have already seen it
In the Java Runtime itself, Desktop.getDesktop() and internal cryptographic provider registries use lazy holder idioms. Spring Boot's BeanFactory resolves and caches singletons with double-checked synchronization. In high-performance caching libraries like Caffeine, segment tables use double-checked locking for eviction counters.
Related patterns
- Singleton vs Factory: Singletons are often retrieved through a static factory method.
- Singleton vs Dependency Injection: Dependency injection containers manage singleton lifecycles externally, avoiding hard-coded static
getInstance()calls.
Try it
Connection Pool LLD
Implement a thread-safe singleton connection pool using double-checked locking.