How to Implement a Thread-Safe Singleton with Double-Checked Locking in Java
Use a volatile instance field combined with two null checks—one outside and one inside a synchronized block—to lazily initialize a Singleton while minimizing synchronization overhead.
The double-checked locking pattern is a widely used optimization for lazy singleton initialization in multithreaded environments. This article examines the production-ready implementation found in the iluwatar/java-design-patterns repository, demonstrating how to correctly apply thread-safe Singleton with double-checked locking in modern Java applications.
Why Double-Checked Locking Matters
Without optimization, every call to getInstance() would require synchronization, creating a bottleneck under high concurrency. The double-checked locking pattern solves this by checking if the instance exists before entering a synchronized block, reducing synchronization overhead to the first initialization only.
Core Implementation Details
The canonical implementation resides in singleton/src/main/java/com/iluwatar/singleton/ThreadSafeDoubleCheckLocking.java. This class demonstrates the precise mechanics required for correct double-checked locking in Java.
The Volatile Field Declaration
At line 39, the instance is declared with the volatile keyword:
private static volatile ThreadSafeDoubleCheckLocking instance;
The volatile modifier ensures that writes to the instance field are immediately visible to all threads, preventing the instruction reordering issues that made double-checked locking broken in early Java versions.
Local Variable Optimization
Following Joshua Bloch's guidance from Effective Java, the implementation uses a local variable at line 58 to cache the volatile field:
ThreadSafeDoubleCheckLocking result = instance;
This technique avoids the performance cost of repeated volatile reads, providing approximately a 25% speed improvement over accessing the volatile field directly in multiple places.
The Double-Check Logic
The method implements two distinct null checks. The first check at line 58 occurs outside any synchronized block:
if (result == null) {
If the instance is null, the code enters a synchronized block at line 65 using the class object as the monitor:
synchronized (ThreadSafeDoubleCheckLocking.class) {
Inside the synchronized block, a second null check at line 70 verifies that another thread did not initialize the instance while the current thread was waiting for the lock:
if (instance == null) {
instance = new ThreadSafeDoubleCheckLocking();
}
Reflection Attack Protection
The private constructor at lines 44-46 includes a guard against reflection attacks:
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
If a malicious actor attempts to invoke the constructor via reflection after the singleton is initialized, the code throws an IllegalStateException, preventing multiple instantiation.
Complete Implementation Example
Here is the full implementation based on the source code:
package com.iluwatar.singleton;
public final class ThreadSafeDoubleCheckLocking {
private static volatile ThreadSafeDoubleCheckLocking instance;
private ThreadSafeDoubleCheckLocking() {
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
}
public static ThreadSafeDoubleCheckLocking getInstance() {
ThreadSafeDoubleCheckLocking result = instance;
if (result == null) {
synchronized (ThreadSafeDoubleCheckLocking.class) {
result = instance;
if (result == null) {
instance = result = new ThreadSafeDoubleCheckLocking();
}
}
}
return result;
}
}
Thread Safety Verification
The repository includes comprehensive tests in singleton/src/test/java/com/iluwatar/singleton/ThreadSafeDoubleCheckLockingTest.java. The test suite creates 10,000 Callable tasks that each invoke getInstance() on separate threads, verifying that every thread receives the identical instance reference. This empirical validation confirms that the double-checked locking implementation correctly handles high-concurrency scenarios without race conditions.
Java Version Requirements
This implementation requires Java 5 or later. The volatile keyword in Java 5 received enhanced semantics that provide the necessary happens-before guarantees for safe double-checked locking. As noted in the source code comments at lines 30-33, this pattern is "Broken under Java 1.4" because earlier JVMs did not prevent instruction reordering of volatile writes.
Summary
- Declare the singleton instance as
volatileto ensure visibility across threads and prevent instruction reordering. - Use a local variable to cache the volatile field, eliminating the performance penalty of repeated volatile reads.
- Implement two null checks: one outside the synchronized block for performance, and one inside to prevent race conditions during initialization.
- Guard the private constructor against reflection attacks by checking if the instance already exists.
- Requires Java 5+ due to the enhanced
volatilesemantics introduced in that version.
Frequently Asked Questions
What is double-checked locking?
Double-checked locking is an optimization pattern that reduces synchronization overhead by first testing the locking criterion without acquiring the lock. Only if the check indicates that locking is necessary does the code synchronize. In singleton implementations, this means checking if the instance is null before entering a synchronized block, then checking again inside the block to ensure thread-safe initialization.
Why must the instance field be declared volatile?
The volatile keyword establishes a happens-before relationship between the write to the instance field and subsequent reads by other threads. Without volatile, the Java Memory Model might allow instruction reordering that could cause a thread to see a partially constructed object. The volatile modifier ensures that the singleton is fully constructed before any thread can access the reference, preventing the "unsafe publication" problem that made double-checked locking fail in Java 1.4.
Does double-checked locking work in all Java versions?
No, double-checked locking only works reliably in Java 5 and later versions. Prior to Java 5, the volatile keyword did not provide sufficient memory visibility guarantees to prevent instruction reordering issues. The implementation in the iluwatar/java-design-patterns repository explicitly notes that it is "Broken under Java 1.4" and requires the enhanced volatile semantics introduced in Java 5 to function correctly.
How does the implementation prevent reflection attacks?
The private constructor includes an explicit guard that checks whether the static instance field is already non-null. If a developer or attacker attempts to create a new instance via reflection after the singleton has been initialized, the constructor detects the existing instance and throws an IllegalStateException with the message "Already initialized." This prevents the creation of multiple instances even when the private constructor is made accessible through setAccessible(true).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →