Thread-Safe Singleton Pattern in Java: 6 Implementation Methods Explained
The most robust ways to implement a thread-safe Singleton pattern in Java include the Enum approach, Static Holder Class idiom, and Double-Checked Locking with volatile, each offering different trade-offs between laziness, performance, and protection against reflection attacks.
The Singleton pattern guarantees that a class has only one instance and provides a global access point to it. In multithreaded environments, implementing a thread-safe Singleton pattern in Java requires careful handling to prevent race conditions during lazy initialization. The CyC2018/CS-Notes repository documents six distinct approaches in notes/设计模式 - 单例.md, ranging from the naïve unsafe version to the reflection-proof Enum singleton.
1. Lazy Initialization (Thread-Unsafe)
The simplest lazy implementation delays instantiation until the first call to getUniqueInstance(), but fails under concurrency.
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton(); // ← race condition
}
return uniqueInstance;
}
}
When two threads simultaneously find uniqueInstance == null, both proceed to create a new object, violating the singleton guarantee. As noted in the source analysis at [lines 23‑36][lazy-unsafe], this version is suitable only for single-threaded demos.
2. Eager Initialization (Static Field)
This approach creates the instance during class loading, eliminating synchronization concerns.
public class Singleton {
private static final Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
The JVM synchronizes class initialization, ensuring the instance is created exactly once before any thread accesses it ([lines 46‑48][eager]). The drawback is the lack of lazy initialization; the instance is created even if never used.
3. Synchronized Accessor Method
Adding synchronized to the getter prevents concurrent creation but introduces performance overhead.
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
This guarantees thread safety by locking the entire method, as shown at [lines 56‑62][sync]. However, every call incurs synchronization overhead, even after the instance is initialized.
4. Double-Checked Locking (DCL)
Double-checked locking minimizes synchronization by checking the instance twice—once without locking and once within a synchronized block.
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
The volatile keyword is critical: it prevents instruction reordering that could expose a partially constructed object to other threads ([lines 71‑88][double-checked]). After initialization, calls incur only a cheap null check, making this the preferred high-performance lazy initialization approach.
5. Static Holder Class Idiom
This pattern leverages the JVM's class loading mechanism to achieve lazy initialization without explicit synchronization.
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
The inner static class SingletonHolder is not loaded until getUniqueInstance() is invoked. Class loading is thread-safe, so the instance is created exactly once without synchronized or volatile ([lines 118‑130][holder]). This is widely considered the best lazy-initialization approach for most applications.
6. Enum Singleton
The Enum approach provides built-in thread safety, serialization safety, and protection against reflection attacks.
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() { return objName; }
public void setObjName(String objName) { this.objName = objName; }
}
The JVM guarantees that enum values are instantiated only once. Serialization automatically returns the same instance, and reflection cannot invoke the private enum constructor ([lines 134‑152][enum]). This is the most robust implementation, recommended by Joshua Bloch in Effective Java.
How to Choose the Right Implementation
| Situation | Recommended Pattern |
|---|---|
| Simplicity, no lazy requirement | Eager (static) |
| Minimal lock overhead, lazy init needed | Static holder |
| Highest performance with lazy init, Java 5+ | Double-checked locking |
| Need absolute safety against reflection/serialization | Enum |
| Educational/demo of thread-unsafe case | Lazy (unsafe) |
| Quick prototype where lock cost is negligible | Synchronized accessor |
Summary
- Lazy (unsafe): Demonstrates the race condition risk in multithreaded environments.
- Eager: Simplest thread-safe approach, sacrificing lazy initialization.
- Synchronized accessor: Easy to implement but incurs unnecessary locking overhead.
- Double-checked locking: High-performance lazy initialization requiring
volatilefor memory visibility. - Static holder class: Lazy initialization without explicit synchronization, leveraging JVM class loading guarantees.
- Enum: The most robust solution, automatically handling thread safety, serialization, and reflection attacks.
Frequently Asked Questions
Why is the volatile keyword necessary in double-checked locking?
Without volatile, the JVM may reorder the steps of object creation (allocate memory, initialize object, assign reference). Another thread could see a non-null reference to an incompletely initialized object. The volatile keyword ensures happens-before semantics, preventing this reordering and guaranteeing visibility across threads.
Can reflection break Singleton patterns?
Yes, reflection can bypass private constructors in most Singleton implementations by calling setAccessible(true) and creating new instances. The Enum Singleton is immune to this attack because the JVM prevents reflection from instantiating enum constants, throwing an IllegalArgumentException if attempted.
Which Singleton implementation is best for Android development?
The Static Holder Class idiom is generally preferred for Android because it provides lazy initialization without the synchronization overhead of double-checked locking or enum overhead. However, if you need protection against serialization or reflection, or if the Singleton holds state that must survive configuration changes, the Enum approach is safer despite its slightly higher memory footprint.
How does the Enum Singleton prevent serialization attacks?
Standard serialization can create new instances of Serializable classes during deserialization. However, when an enum is deserialized, the JVM ensures that the same constant instance is returned rather than creating a new object. This is handled automatically by the readObject and readResolve mechanisms built into the enum type, making it inherently serialization-safe without boilerplate code.
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 →