Design Patterns in Java: A Detailed Explanation of the Singleton Pattern
Description
The Singleton Pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. This pattern is particularly useful in scenarios where controlled access to resources is needed, such as database connection pools, configuration managers, loggers, etc.
Core Points
- Private Constructor: Prevents external instantiation via the
newkeyword. - Static Private Member Variable: Stores the single instance.
- Static Public Method: Provides the global access point.
Evolution Process
1. Eager Initialization
public class Singleton {
// Instance is created when the class is loaded
private static final Singleton INSTANCE = new Singleton();
// Private constructor
private Singleton() {}
// Global access point
public static Singleton getInstance() {
return INSTANCE;
}
}
- Advantages: Simple implementation, thread-safe.
- Disadvantages: Can waste memory if the instance is never used.
2. Lazy Initialization
public class Singleton {
private static Singleton instance;
private Singleton() {}
// Instance is created only when needed
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- Advantages: Lazy loading, saves memory.
- Disadvantages: Not thread-safe; multiple threads might create separate instances.
3. Thread-Safe Lazy Initialization
public class Singleton {
private static Singleton instance;
private Singleton() {}
// Synchronized method ensures thread safety
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- Advantages: Thread-safe.
- Disadvantages: Synchronization overhead on every call, impacting performance.
4. Double-Checked Locking
public class Singleton {
// `volatile` prevents instruction reordering
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // First check
synchronized (Singleton.class) {
if (instance == null) { // Second check
instance = new Singleton();
}
}
}
return instance;
}
}
- First Check: Avoids unnecessary synchronization.
- Second Check: Ensures only one thread creates the instance.
volatileKeyword: Prevents null pointer issues caused by JVM instruction reordering.
5. Static Inner Class (Holder) Method
public class Singleton {
private Singleton() {}
// The static inner class is loaded only when first used
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- Advantages: Thread-safe, lazy loading, good performance.
- Principle: Leverages the class loading mechanism to ensure thread safety.
6. Enum Method (Recommended)
public enum Singleton {
INSTANCE;
public void doSomething() {
// Business method
}
}
- Advantages: Thread-safe, prevents reflection attacks, prevents deserialization from creating new instances.
- This is the method recommended by Joshua Bloch, author of Effective Java.
Key Issues Explained
1. Why are two null checks needed?
- First Check: Avoids unnecessary synchronization, improving performance.
- Second Check: Ensures only one thread can create the instance inside the synchronized block.
2. The Role of the volatile Keyword
instance = new Singleton(); // This line involves 3 steps:
// 1. Allocate memory space
// 2. Initialize the object
// 3. Point `instance` to the memory address
Without volatile, instruction reordering might occur, leading to other threads accessing an uninitialized object.
3. Preventing Reflection Attacks
public class Singleton {
private static boolean initialized = false;
private Singleton() {
synchronized (Singleton.class) {
if (initialized) {
throw new RuntimeException("Singleton pattern violated!");
}
initialized = true;
}
}
}
Use Cases
- Objects that need to be frequently created and destroyed.
- Objects that are expensive to create or consume significant resources.
- Utility class objects.
- Resource managers like database connection pools.
Each implementation has its appropriate use case; the most suitable one should be chosen based on specific requirements.