No history yet

Lazy Singleton Implementation

Double-Checked Locking and Memory Models

In high-concurrency scenarios, the simple synchronized getInstance() method for a Singleton introduces a performance bottleneck. Once the instance is created, the lock is unnecessary but still incurs overhead. Double-Checked Locking (DCL) was devised as an optimization to avoid this.

The initial check if (instance == null) happens without a lock. Only if the instance appears to be null does the thread acquire a lock and check again. This second check is crucial to prevent multiple threads from creating separate instances if they pass the first check simultaneously.

public class DclSingleton {
    // The volatile keyword is essential here
    private static volatile DclSingleton instance;

    private DclSingleton() {}

    public static DclSingleton getInstance() {
        if (instance == null) { // First check (no lock)
            synchronized (DclSingleton.class) {
                if (instance == null) { // Second check (with lock)
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
}

Without the volatile keyword, this pattern is broken. The (JMM) allows for instruction reordering by the compiler and CPU to optimize performance. A non-volatile write to instance can be reordered with the internal steps of the DclSingleton constructor.

This means a thread could see a non-null reference to instance before the object's constructor has finished executing. The result? The thread gets a reference to a partially constructed, unstable object. The volatile keyword enforces a happens-before relationship, guaranteeing that the write to the instance variable will not be reordered with the constructor's execution. It also ensures that changes to the instance variable are immediately visible to all other threads.

The Bill Pugh Solution

While DCL works, its reliance on volatile is subtle and was even broken in earlier versions of Java. A cleaner, more reliable approach is the Initialization-on-Demand Holder idiom, commonly known as the Bill Pugh Singleton. This pattern leverages the guarantees of class loading to achieve thread-safe lazy initialization without any explicit synchronization or volatile keywords.

public class BillPughSingleton {

    private BillPughSingleton() {}

    private static class SingletonHolder {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Here, the nested static class SingletonHolder is not loaded into memory until the getInstance() method is called for the first time. The JVM guarantees that class initialization is a serial, thread-safe process. When the first thread invokes getInstance(), the JVM loads and initializes SingletonHolder, creating the INSTANCE. Subsequent calls simply return the already-created instance without any locking overhead. This is generally the preferred approach for lazy initialization in modern Java.

Enums and Serialization

For the highest level of robustness, especially in distributed systems that rely on serialization, the Enum Singleton is the clear winner. While the Bill Pugh implementation is excellent, it can be compromised by reflection or improper serialization, both of which can lead to multiple instances.

public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        // ... singleton business logic
    }
}

This approach is concise and provides ironclad protection against multiple instantiations, even in the face of complex serialization or reflection attacks. The JVM guarantees that enum constants are instantiated only once. When an enum is serialized, only its name is written. Upon deserialization, the JVM invokes the valueOf() method with the name to return the pre-existing instance, ensuring the singleton contract is never violated. This built-in behavior obviates the need to manually implement a readResolve() method, which would be required to protect other singleton implementations from deserialization-based attacks.

Now, let's test your understanding of these advanced concurrency patterns.

Quiz Questions 1/5

In a high-concurrency scenario, why is a simple synchronized method for implementing a Singleton often considered a performance bottleneck?

Quiz Questions 2/5

In the context of the Double-Checked Locking (DCL) pattern for Singletons, what is the primary role of the volatile keyword?