☕ Java Q70 / 162

Explain double-checked locking.

AI-Powered Answer ✓ Answered

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

java
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.

java
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 Singleton object.
  • 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 instance are immediately visible to all threads.
  • Ordering: It prevents instruction reordering concerning the volatile field. Specifically, writes to a volatile variable happen-before subsequent reads of that volatile variable, ensuring that the assignment of the initialized object to instance is not reordered before its construction.
java
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.
java
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 enum with 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.