Fundamentals 7 min read

Why Double‑Checked Locking Fails in Java and How to Fix It

This article explains why the classic Double‑Checked Locking implementation of a lazy singleton is unsafe in Java, analyzes the underlying object‑creation reordering problem, and presents two reliable solutions—using a volatile field or a static holder class—to achieve thread‑safe lazy initialization.

Programmer DD
Programmer DD
Programmer DD
Why Double‑Checked Locking Fails in Java and How to Fix It

1. Problem Analysis

The classic lazy‑initialization singleton uses a simple null check and creates the instance inside getInstance(). This version is not thread‑safe because multiple threads can create separate instances.

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

Adding synchronized to the whole method makes it safe but incurs a performance penalty, which motivated the Double‑Checked Locking (DCL) pattern.

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

If the first singleton != null check succeeds, the costly lock is avoided, improving performance.

If the first check fails, the lock guarantees that only one thread creates the instance.

After the instance is created, the second check bypasses the lock for subsequent calls.

Despite appearing correct, DCL is unsafe because the Java Memory Model allows reordering of the object‑creation steps. The three steps of creating an object are:

Allocate memory.

Initialize the object.

Assign the reference to the variable.

Due to reordering, steps 2 and 3 may be swapped, so another thread can see a non‑null reference before the constructor finishes. The following diagram (originally from the article) illustrates this issue:

Reordering example where thread B sees a partially constructed singleton
Reordering example where thread B sees a partially constructed singleton

The root cause is the assignment statement:

singleton = new Singleton();

Two categories of solutions exist:

Prevent steps 2 and 3 from being reordered.

Allow reordering but ensure that other threads cannot observe the intermediate state.

2. Solutions

Volatile‑based solution

Mark the singleton field as volatile. The volatile keyword forces a happens‑before relationship that stops the reordering of the write of the reference with the object’s initialization.

public class Singleton {
    // volatile guarantees visibility and ordering
    private volatile static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

With volatile, steps 2 and 3 cannot be reordered, eliminating the unsafe window.

Class‑initialization holder solution

Leverage the JVM’s class‑initialization lock by placing the instance in a static inner class. The JVM acquires a lock during class initialization, guaranteeing that only one thread creates the instance.

public class Singleton {
    private Singleton() {}
    private static class SingletonHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

This approach lets the JVM handle synchronization, and the instance is created lazily when SingletonHolder is first accessed.

Java specifies that each class or interface C has a unique initialization lock LC. The JVM acquires this lock during class initialization, ensuring that at most one thread performs the initialization.

Both solutions provide a correct, thread‑safe lazy‑initialization pattern without the pitfalls of the original DCL implementation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyvolatileSingletondouble-checked locking
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.