singleton pattern
design patterns
double checked locking
multithreading
software development

Double Checked Locking in Singleton

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Double-checked locking is a pattern for lazily creating a singleton without paying synchronization cost on every read. It is widely discussed because the idea is simple, but the implementation is only safe when it respects the memory model. In modern Java, that means using volatile.

What the Pattern Tries to Achieve

A normal synchronized singleton is easy to understand:

java
1public class SimpleSingleton {
2    private static SimpleSingleton instance;
3
4    public static synchronized SimpleSingleton getInstance() {
5        if (instance == null) {
6            instance = new SimpleSingleton();
7        }
8        return instance;
9    }
10}

This is correct, but every call acquires the class monitor. Double-checked locking tries to avoid that overhead after initialization by checking once before locking and again inside the synchronized block.

java
1public class SettingsManager {
2    private static volatile SettingsManager instance;
3
4    private SettingsManager() {
5    }
6
7    public static SettingsManager getInstance() {
8        if (instance == null) {
9            synchronized (SettingsManager.class) {
10                if (instance == null) {
11                    instance = new SettingsManager();
12                }
13            }
14        }
15        return instance;
16    }
17}

The outer check avoids locking once the singleton already exists. The inner check ensures only one thread performs construction if several threads race into the first branch.

Why volatile Matters

Without volatile, the code may publish a reference to an object before construction is fully visible to other threads. That sounds abstract, but the effect is real: another thread can observe a non-null reference to a partially initialized object.

The broken version looks almost identical:

java
1public class BrokenSingleton {
2    private static BrokenSingleton instance;
3
4    public static BrokenSingleton getInstance() {
5        if (instance == null) {
6            synchronized (BrokenSingleton.class) {
7                if (instance == null) {
8                    instance = new BrokenSingleton();
9                }
10            }
11        }
12        return instance;
13    }
14}

The code may pass tests for a long time because concurrency bugs are timing-sensitive. That does not make it safe. The volatile modifier is what gives the publication and visibility guarantees the pattern needs.

When Double-Checked Locking Is Reasonable

Use this pattern only if two conditions are true:

  • lazy initialization actually matters
  • singleton access happens often enough that the extra complexity is worth it

In many applications, neither condition holds. A simpler eager singleton is often the better engineering choice.

java
1public class EagerSingleton {
2    private static final EagerSingleton INSTANCE = new EagerSingleton();
3
4    private EagerSingleton() {
5    }
6
7    public static EagerSingleton getInstance() {
8        return INSTANCE;
9    }
10}

This version is thread-safe because class initialization in Java is already synchronized by the runtime.

Better Alternatives in Java

If you want lazy initialization without the mental overhead of DCL, the holder idiom is usually cleaner.

java
1public class HolderSingleton {
2    private HolderSingleton() {
3    }
4
5    private static class Holder {
6        private static final HolderSingleton INSTANCE = new HolderSingleton();
7    }
8
9    public static HolderSingleton getInstance() {
10        return Holder.INSTANCE;
11    }
12}

This relies on class loading semantics and avoids explicit synchronization in the accessor.

For truly global singletons, an enum can be even simpler:

java
public enum AppConfig {
    INSTANCE;
}

The enum approach is concise and also handles serialization concerns more safely than many hand-written singleton implementations.

Performance and Design Tradeoffs

Developers sometimes choose double-checked locking because it feels like a micro-optimization they "should" apply. In practice, that is rarely the right starting point. Modern JVMs handle uncontended synchronization well, and a singleton accessor is seldom the true bottleneck in a business application.

The bigger risk is design quality. A concurrency-safe singleton can still be poor architecture if it becomes a hidden global store for mutable state. If your singleton contains caches, configuration, or clients that make testing difficult, dependency injection may be a better model than a global accessor.

Common Pitfalls

  • Implementing double-checked locking without volatile.
  • Using the pattern when eager initialization or the holder idiom would be simpler.
  • Assuming the code is safe because it "worked fine" in local tests.
  • Hiding mutable global state behind a singleton and making tests harder.
  • Optimizing singleton access before measuring whether it matters.

Summary

  • Double-checked locking is safe in modern Java only when the instance field is volatile.
  • The pattern reduces locking after initialization, but adds complexity.
  • Eager initialization, holder idiom, or enum singletons are often clearer choices.
  • Correct publication matters more than clever-looking concurrency code.
  • Choose a singleton pattern based on measured need, not habit.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.