Low Level Design

Singleton Pattern — Thread-Safe Implementations in Java

A deep dive into the Singleton design pattern: why it exists, common pitfalls, and three progressively better thread-safe implementations.

July 10, 2025·10 min read

What is the Singleton Pattern?#

The Singleton is a creational design pattern that ensures a class has only one instance and provides a global access point to it.

You reach for Singleton when:

  • A single shared resource must exist (database connection pool, config manager, logger)
  • Creating multiple instances would cause incorrect behaviour or waste resources

Class Diagram#


Naive Implementation (Not Thread-Safe)#

java
public class Singleton {
    private static Singleton instance;

    private Singleton() {}          // prevent external instantiation

    public static Singleton getInstance() {
        if (instance == null) {     // ← race condition here in multi-threaded code
            instance = new Singleton();
        }
        return instance;
    }

    public void doWork() {
        System.out.println("Singleton doing work.");
    }
}

Problem: Two threads can both see instance == null and create two separate objects.


Fix 1 — Synchronized Method#

java
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Trade-off: Correct but slow — every getInstance() call acquires a lock, even after initialisation.


java
public class Singleton {
    // volatile ensures visibility of writes across threads
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // first check (no lock)
            synchronized (Singleton.class) {
                if (instance == null) {          // second check (with lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

The volatile keyword prevents the JVM from reordering the write to instance before the constructor finishes, which is the subtle bug DCL had before Java 5.


Fix 3 — Initialization-on-Demand Holder (Cleanest)#

java
public class Singleton {

    private Singleton() {}

    // Inner class is loaded only on first access to getInstance()
    private static final class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

This leverages the JVM's class-loading guarantee: a class is initialised exactly once, lazily, and in a thread-safe manner — no synchronisation overhead.


Extending the Design — Registry of Singletons#

When you need multiple named singletons (e.g., per-tenant configurations):

java
import java.util.concurrent.ConcurrentHashMap;

public class SingletonRegistry {
    private static final ConcurrentHashMap<String, Singleton> registry =
            new ConcurrentHashMap<>();

    public static Singleton getInstance(String key) {
        return registry.computeIfAbsent(key, Singleton::new);
    }

    public static class Singleton {
        private final String name;

        private Singleton(String name) {
            this.name = name;
        }

        public String getName() { return name; }
    }
}

computeIfAbsent is atomic — no race condition possible.


When NOT to Use Singleton#

SituationWhy to avoid
Unit testingHidden global state makes test isolation hard
Dependency injection containersThe container already manages lifecycle
Stateful singletons in distributed systemsEach JVM gets its own instance — not truly single

Summary#

ImplementationLazyThread-safeNotes
Naive✅❌Never use in prod
Synchronized method✅✅Simple but slow
Double-checked locking✅✅Requires volatile
Holder idiom✅✅Cleanest, preferred

Complete Implementation#

Here are all the files needed to build the Singleton Registry pattern. Browse each tab to see how the classes fit together:

java
public final class Singleton {

    /** Lazy, thread-safe via class-loading guarantee. */
    private static final class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    public void doWork() {
        System.out.println("Working: " + this);
    }
}