Back to Creational: Singleton, Factory, Builder, Prototype
Q8Question 8 of 12

What is the Singleton pattern and how to write a thread-safe lazy implementation

Key Concepts
  • Lazy initialization builds the instance only on first use, saving resources when construction is expensive.
  • Eager initialization is thread-safe via class loading but wastes resources if the instance is never used.
  • Synchronized getter is lazy and correct but locks every call, even after the instance exists.
  • Double-checked locking checks null without a lock, then locks and checks again; requires volatile to prevent partial construction.
  • Bill Pugh holder uses a private static inner class; the JVM's class-initialization guarantee makes it lazy and thread-safe with no explicit synchronization.
  • Enum singleton is immune to serialization, reflection, and cloning; Effective Java calls it the best approach.

Definition

A lazy thread-safe Singleton builds the single instance only on first use, safely under concurrency, and the cleanest ways are the Bill Pugh holder idiom and the enum.
  • Singleton – guarantees one instance for the entire application with a single access point.
  • Lazy initialization – creates the instance only on first use, important when construction is expensive.
  • Thread safety – concurrent threads must not be able to create two instances.
  • Bill Pugh holder – lazy + safe via JVM class-initialization guarantee, no synchronization needed.
  • Enum singleton – most robust: immune to serialization, reflection, and cloning by JVM design.

The National Bank of India keeps one shared audit ledger for the whole bank. There must never be two ledgers, because two ledgers could record conflicting versions of the same transaction. The bank also prefers to open this ledger only when the first transaction actually needs it, and when dozens of tellers reach for it at the same second, exactly one ledger must be created and shared.

Understanding the Concepts

Why does lazy initialization matter?

Lazy initialization means not building the instance until first use. This matters when construction is costly, such as loading a large audit configuration, and the instance might not be needed in every run.

How does eager initialization work?

JAVA
package com.bank.config;

public class AuditLedger {
    private static final AuditLedger INSTANCE = new AuditLedger();

    private AuditLedger() {
        System.out.println("AuditLedger loaded");
    }

    public static AuditLedger getInstance() {
        return INSTANCE;
    }

    public void log(String entry) {
        System.out.println("AUDIT: " + entry);
    }
}

Thread-safe because the JVM initialises a class once under a lock. Not lazy: the instance is created at class load.

How does the synchronized getter work?

JAVA
package com.bank.config;

public class AuditLedgerSync {
    private static AuditLedgerSync instance;

    private AuditLedgerSync() {}

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

Lazy and correct, but every call locks, even after the instance exists. Under heavy concurrent reads, this is a bottleneck.

How does double-checked locking work?

JAVA
package com.bank.config;

public class AuditLedgerDCL {
    private static volatile AuditLedgerDCL instance;

    private AuditLedgerDCL() {}

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

The volatile keyword is essential. Without it, instance = new AuditLedgerDCL() is not atomic: the JVM may allocate memory, assign the reference, then construct the object. Another thread sees a non-null reference pointing to a partially-constructed object. With volatile, this reordering is forbidden.

How does the Bill Pugh holder work?

JAVA
package com.bank.config;

public class AuditLedgerBillPugh {

    private AuditLedgerBillPugh() {
        System.out.println("AuditLedger created (Bill Pugh)");
    }

    private static class Holder {
        static final AuditLedgerBillPugh INSTANCE = new AuditLedgerBillPugh();
    }

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

The outer class loading does not initialise the holder. The holder is loaded only when getInstance() first references Holder.INSTANCE. Because the JVM performs class initialization exactly once under its own lock, this is both lazy and thread-safe with no volatile and no synchronized.

How does the enum singleton work?

JAVA
package com.bank.config;

import java.math.BigDecimal;
import java.util.Map;

public enum RateRegistry {
    INSTANCE;

    private final Map<String, BigDecimal> rates = Map.of(
        "SAVINGS", new BigDecimal("0.04"),
        "CURRENT", new BigDecimal("0.02")
    );

    public BigDecimal getRate(String type) {
        return rates.getOrDefault(type, BigDecimal.ZERO);
    }
}

The JVM guarantees a single instance. Enums are safe against serialization, reflection, and cloning. Joshua Bloch calls this the best way. Its only limitation: an enum cannot extend a class.

How do you defend against serialization, reflection, and cloning?

JAVA
package com.bank.config;

import java.io.Serializable;

public class SecureAuditLedger implements Serializable {
    private static final SecureAuditLedger INSTANCE = new SecureAuditLedger();

    private SecureAuditLedger() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Use getInstance()");
        }
    }

    public static SecureAuditLedger getInstance() { return INSTANCE; }

    private Object readResolve() { return INSTANCE; }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton");
    }
}

The enum eliminates all three guards for free.

Which approach should you choose?

| Approach | Lazy | Thread-safe | Serialization-safe | Reflection-safe |
|---|---|---|---|---|
| Eager | No | Yes | No | No |
| Synchronized | Yes | Yes | No | No |
| DCL (volatile) | Yes | Yes | No | No |
| Bill Pugh | Yes | Yes | No (add readResolve) | No (add guard) |
| Enum | Yes | Yes | Yes | Yes |

What should a senior engineer know?

  • A JVM Singleton is one per JVM, not one per cluster. Across a cluster, use a shared store.
  • In Spring, beans are singletons by default via the container scope, not via the Singleton pattern.
  • Singletons carry global state and make unit testing harder. Prefer DI in production.
JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class SingletonLazyDemo {
    public static void main(String[] args) {
        // Enum
        BigDecimal rate = RateRegistry.INSTANCE.getRate("SAVINGS");
        System.out.println("Savings rate: " + rate);

        // Bill Pugh
        AuditLedgerBillPugh ledger = AuditLedgerBillPugh.getInstance();
        ledger.log("Transfer TXN-001 completed");
    }
}
TEXT
Savings rate: 0.04
AuditLedger created (Bill Pugh)
AUDIT: Transfer TXN-001 completed

Visual Overview

DIAGRAM
  Singleton Thread-Safety Approaches
  ──────────────────────────────────

  Eager:       class load ──▶ INSTANCE created (always)
  Sync getter: every call ──▶ LOCK ──▶ null check ──▶ create/return
  DCL:         null check ──▶ null? LOCK ──▶ check again ──▶ create
                              not null? ──▶ return (fast)
  Bill Pugh:   getInstance() ──▶ touch Holder class
                                  ──▶ JVM loads once ──▶ create
  Enum:        INSTANCE ──▶ JVM guarantees one
                          ──▶ immune to clone/serial/reflect

  Recommended:
  ┌────────────┬───────────────────────┐
  │ Enum       │ Default choice        │
  │ Bill Pugh  │ When extending a class│
  └────────────┴───────────────────────┘

Key Takeaway

The Singleton pattern guarantees one instance for the whole application. A thread-safe, lazily initialised Singleton creates that instance only on first use while preventing concurrent threads from creating duplicates.

Rajesh walks through the options from weakest to strongest. Eager initialization is thread-safe but not lazy. The synchronized getter is lazy but locks every call. Double-checked locking locks only on first creation but requires volatile to prevent another thread from seeing a partially-constructed object.

The two Rajesh prefers are the Bill Pugh holder and the enum. Bill Pugh uses a private static inner class that the JVM loads only when getInstance() touches it, giving lazy, thread-safe creation with no synchronized or volatile. The enum declares a single constant that the JVM guarantees is one instance, immune to serialization, reflection, and cloning, which is why Effective Java recommends it.

At National Bank of India, Pankaj uses the enum for the shared rate registry and the Bill Pugh holder for the audit ledger that needs special constructor logic. He also remembers that a JVM Singleton is one per JVM, not one per cluster.

Remember one sentence: use the enum for free safety against serialization, reflection, and cloning, and the Bill Pugh holder when you need lazy class-based construction with no explicit synchronization.

---

Share
Use ← → arrow keys to navigate