Understanding the four core principles of singleton patterns:
- Private constructor.
- Return instance via static method or enumeration.
- Ensure only one instance exists, especially in multi-threaded environments.
- Prevent object reconstruction during deserialization.
Common singleton implementations include:
Eager initialization, lazy initialization, double-checked locking (DCL), static inner class, and enumeration-based approaches. Let's examine each pattern in detail.
- Eager Initialization:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
Eager initialization creates the object in memory as soon as the class loads, trading space for time, thus avoiding thread safety issues.
- Lazy Initialization:
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
Lazy initialization delays object creation until the method is invoked, trading time for space. However, it introduces risks in multi-threaded scenarios.
- Double-Checked Locking (DCL):
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
The DCL pattern initializes the object only when needed. The first null check avoids unnecessary synchronization. Only upon first access does the lock occur before instantiation. This approach saves memory and ensures thread safety. However, due to JVM instruction reordering, DCL can lead to thread safety issues. Here’s what happens under the hood:
INSTANCE = new Singleton();
This line involves three steps within the JVM:
- Allocate memory for the object.
- Initialize fields of the object.
- Assign reference to the allocated memory.
Instruction reordering might cause step 3 to execute before step 2. If another thread accesses the instance at this point, it may use an uninitialized object, leading to errors — known as the DCL failure issue.
Since JDK 1.5, the volatile keyword has been introduced to resolve this. By declaring INSTANCE as private volatile static Singleton INSTANCE = null;, we ensure that reads and writes always happen from the main memory, which solves the DCL problem, albeit at a slight performance cost.
- Static Inner Class:
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
The static inner class offers benefits: the outer class loading doesn't immediately trigger inner class loading. The INSTANCE is not initialized unless the inner class is loaded. Therefore, no memory is consumed until the first call to getInstance(). When getInstance() is called, the virtual machine loads SingletonHolder, initializing INSTANCE. This approach guarantees both thread safety and single-instance behavior, and defers instantiation.
How does this ensure thread safety? First, let's understand class loading triggers:
Class loading occurs in exactly five scenarios:
- When encountering
new,getstatic,setstatic, orinvokestaticbytecode instructions — i.e., creating objects, accessing static fields (excluding compile-time constants), or calling static methods. - Using reflection through
java.lang.reflectpackage to invoke a class. - Initializing a subclass when its superclass hasn't been initialized yet.
- Starting the JVM and specifying the main class with the
main()method. - Using dynamic language features in JDK 1.7+, where a
MethodHandleresolves to a static field/method and the class hasn't been initialized.
These five cases are known as active references. All other references are passive. Static inner classes fall into the passive reference category.
Looking back at getInstance(), it accesses SingletonHolder.INSTANCE, which refers to the static field inside the inner class. Unlike DCL, there's no repeated object creation. Regardless of how many threads call getInstance(), they all retrieve the same instance. Only upon the first call does the virtual machine replace symbolic references with direct ones, effectively initializing INSTANCE. This mirrors eager initialization in terms of instance creation.
The thread safety during INSTANCE creation is ensured by the JVM. The <clinit>() method of a class is synchronized across multiple threads. If several threads try to initialize the same class simultaneously, only one will execute the <clinit>() method, while others wait. If the initialization takes too long, this can block other threads. However, once <clinit>() completes, other threads won’t re-enter it again, ensuring each type is initialized only once per loader.
Thus, the static inner class singleton maintains thread safety, uniqueness, and delayed instantiation.
Is the static inner class pattern the ultimate solution? Not entirely. A major drawback is parameter passing — since the singleton is created through a static inner class, external parameters like Context cannot be passed. Thus, developers must weigh whether to use static inner class or DCL based on their needs.
Using the class loader mechanism ensures that only one thread initializes the instance, making it thread-safe without performance overhead.
Advantages:
- Combines lazy loading benefits (initialization on demand) with eager loading safety (resistance to reflection attacks).
Disadvantages:
- Requires two classes. Although the static inner class object isn't instantiated, its class object still gets created and resides in the permanent generation.
- Once destroyed, the singleton cannot be recreated.
Lastly, let's briefly touch upon enum-based singletons.
Enum Singleton:
public enum Singleton {
INSTANCE;
public void doSomething() {
// TODO
}
}
In Java, enums behave like regular classes, supporting fields and methods. Enum instances are thread-safe and always represent a single instance. We can access them directly using:
Singleton.INSTANCE