Explain double-checked locking.
Double-Checked Locking (DCL) is a software design pattern used to reduce the overhead of acquiring a lock when implementing lazy initialization for a multithreaded singleton class. It attempts to ensure that only one instance of a class is created, while avoiding synchronization for every access after the first.
What is Double-Checked Locking?
Double-Checked Locking is an optimization technique aimed at minimizing synchronization overhead in scenarios where an expensive resource needs to be lazily initialized in a thread-safe manner. Its primary goal is to provide a thread-safe singleton pattern while maintaining high performance by reducing the frequency of locking.
The Problem DCL Solves: Thread-Safe Lazy Initialization
Consider a singleton class where creating the instance is computationally expensive, and we only want to create it when it's first needed (lazy initialization). In a multithreaded environment, multiple threads could simultaneously check if the instance is 'null', leading to race conditions and potentially multiple instances being created or a partially constructed instance being used.
Naive, Non-Thread-Safe Lazy Initialization
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // Not thread-safe!
instance = new Singleton();
}
return instance;
}
}
Thread-Safe but Inefficient Naive Synchronization
A simple way to make the getInstance method thread-safe is to synchronize it. However, this imposes a performance penalty, as every call to getInstance will acquire and release a lock, even after the instance has been created.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() { // Synchronized method
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Double-Checked Locking (DCL) - The Concept
DCL attempts to solve the inefficiency of full synchronization by performing a 'double-check' on the instance variable. The first check is outside the synchronized block to avoid the overhead if the instance already exists. If the instance is null, then the code enters a synchronized block for the second check.
The second check *inside* the synchronized block is crucial. It ensures that if multiple threads passed the first null check and entered the synchronized block, only the first one will actually create the instance. Subsequent threads will find the instance non-null during this inner check and exit without creating another instance.
The Issue with DCL before Java 5 (Memory Model)
Before Java 5, DCL was often considered broken due to issues with instruction reordering by the compiler and CPU. The operation instance = new Singleton() is not atomic; it involves three conceptual steps:
- Allocate memory for a new
Singletonobject. - Initialize the object's fields (constructor execution).
- Assign the newly created object's reference to
instance.
A CPU or compiler could reorder steps 2 and 3. This means instance could point to a memory location before the object is fully initialized. If another thread then sees a non-null instance (because step 3 happened) but the object isn't fully initialized (because step 2 hasn't happened yet), it could return a partially constructed object, leading to subtle and hard-to-debug bugs.
Corrected DCL with `volatile` (Java 5+)
Java 5 introduced a stronger memory model, and specifically, the volatile keyword was enhanced to make DCL safe. By declaring the instance variable as volatile, two guarantees are provided:
- Visibility: Changes to
instanceare immediately visible to all threads. - Ordering: It prevents instruction reordering concerning the
volatilefield. Specifically, writes to avolatilevariable happen-before subsequent reads of thatvolatilevariable, ensuring that the assignment of the initialized object toinstanceis not reordered before its construction.
public class Singleton {
private static volatile Singleton instance; // volatile keyword is crucial
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // First check: without lock
synchronized (Singleton.class) { // Acquire lock
if (instance == null) { // Second check: inside lock
instance = new Singleton(); // Object creation
}
}
}
return instance;
}
}
When to Use DCL (and alternatives)
While volatile makes DCL technically correct in Java 5 and later, it's generally considered an advanced optimization. The complexity of understanding and implementing it correctly, combined with the availability of simpler, equally efficient, and safer alternatives, means DCL is rarely the first choice for implementing singletons.
Recommended Alternatives for Singleton
- Initialization-on-demand holder idiom (LazyHolder): This is widely considered the best approach for lazy, thread-safe singletons in Java. It leverages the Java Language Specification's guarantee that a class's static initializer will only be run once by the first thread to reference the class, providing both lazy loading and thread safety without explicit synchronization or
volatile.
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- Enum Singleton: For simple singletons, an
enumwith a single element is arguably the cleanest and most robust way. It inherently handles serialization, prevents reflection-based instantiation, and provides thread safety by design. - Eager Initialization: If the singleton's creation cost isn't prohibitive, or it's always needed, creating the instance directly in a static initializer is the simplest and safest approach.
DCL should only be considered when profiling explicitly indicates a performance bottleneck due to synchronization in a lazy-initialized scenario, and simpler alternatives are not suitable or show measurable performance disadvantages.