Intermediate14 min readConcurrency & Thread Safetylive prototype

Thread-safe Singleton

The classic if (instance == null) instance = new X(); works perfectly — until two threads run it at the same instant and both see null. Then you get two singletons, which is a contradiction in terms. This lesson walks the four standard fixes in order — a lock on the whole method, double-checked locking with volatile, and the static holder idiom — and shows exactly why the last one is usually the right answer.

The idea

What it is

A Singleton's whole promise is one instance. The textbook lazy version keeps that promise beautifully in a single-threaded program: if (instance == null) instance = new Printer(); runs once, and every later call returns the stored object. Put two threads on it, though, and the promise quietly breaks — because that one innocent line is really three separate steps (read the field, build the object, write the field), and another thread can slip in between any two of them.

Picture the office again. Two colleagues, Ana and Ben, both need the shared printer. Each walks to the corner, sees it's empty, and thinks "there's no printer here — I'll order one." Neither knows the other is doing the same thing at that exact moment. An hour later, two printers arrive. Nobody did anything wrong individually; the problem is that checking and acting were two separate moments, and the world changed in between. That gap is called a race condition, and it is the entire subject of this lesson.

The one sentence to remember

Lazy creation is only safe if the check and the create happen as one indivisible step. Every fix below is just a different way of making that true: hold a lock while you do both, or let the language's class loader do it for you.

This is a top-5 interview question

"Write a thread-safe Singleton" is asked constantly, and the interviewer is almost never after the code — they want to hear why double-checked locking needs volatile, and why the static holder idiom (or an enum) is preferred over it. If you can say those two things clearly, you have answered the question.

Mechanics

How it works

1 · Naive lazy — the broken baseline

Here is the version everyone writes first. It has no protection of any kind:

Naive.java
public static Printer getInstance() {
    if (instance == null) {          // ① T1 reads null ... and so does T2
        instance = new Printer();    // ② both build. TWO instances exist.
    }
    return instance;                 // ③ each thread returns a different object
}

Run the prototype in ❌ Naive mode and you can watch it happen: T1 reads the field and sees null, then T2 reads it before T1 has written anything and also sees null. Both enter the branch, both call new Printer(), and 🏭 instances created ticks to 2. Each thread walks away holding a different object with a different queue — which is precisely the bug a Singleton exists to prevent. Worse, it's intermittent: on most runs the timing works out fine, so this bug ships to production and shows up once a week under load.

2 · synchronized — correct, but you pay forever

The obvious fix is a lock: put synchronized on the method so only one thread can be inside it at a time. A lock is a key to a room — you can't enter until whoever has the key comes out. Now T1 takes the key, checks, builds, and leaves; T2 waits, gets the key, checks, sees the instance already exists, and returns it. One instance, guaranteed.

The cost is that the lock is on every call, forever. In the prototype's 🐢 synchronized mode, run all three rounds: 🔒 lock acquisitions climbs to 6 even though the instance was built on the very first one. Rounds 2 and 3 acquire and release a lock purely to read a field that will never change again — and T2 still blocks in amber, waiting its turn. On a hot path called from many threads, that queue at the door becomes real contention.

3 · Double-checked locking — lock only when it might matter

Double-checked locking (DCL) removes the everyday cost by checking twice: once cheaply without the lock, and once again inside it. Think of a window in the supply-room door — you look through the window first (free), and only if the corner looks empty do you bother taking the key, and then you look again inside the room before ordering anything.

DoubleChecked.java
private static volatile Printer instance;      // volatile is NOT optional

public static Printer getInstance() {
    if (instance == null) {                    // ① fast path, no lock
        synchronized (Printer.class) {         // ② only the first callers get here
            if (instance == null) {            // ③ check AGAIN, now under the lock
                instance = new Printer();
            }
        }
    }
    return instance;                           // later calls never touch the lock
}

The second check is the one that does the work. Two threads can both fail check ①, but only one of them gets the lock; by the time the loser is let in, check ③ is false and it just returns what's already there. After that first call, every future call fails check ① immediately and returns without touching the lock at all. In the prototype's ⚡ DCL + volatile mode, the lock counter moves during round 1 and then stops rising — that's the whole payoff.

Drop volatile and DCL silently breaks

volatile means: every thread always reads and writes this field in main memory, in program order — no caching, no reordering. Without it, the compiler and CPU are allowed to reorder instance = new Printer() so the field is set to the new address before the constructor has finished filling in the object's fields. A second thread then passes the lock-free check ①, sees a non-null reference, skips the lock entirely and returns a half-built object — non-null, but with empty fields. Flip the volatile: ON/OFF switch in the prototype and run it: T2's lane turns red with ⚠️ half-built object visible. This is not theoretical; before Java 5 fixed the memory model, DCL was formally declared broken.

4 · The static holder idiom — lazy AND lock-free

Here's the punchline: you can get lazy initialisation with no lock and no null check at all, by making the language do it. Every JVM already guarantees that a class is initialised exactly once, and that all threads see the finished result — it takes an internal lock during class loading and then never again. So put the instance in a tiny private nested class that isn't loaded until someone actually touches it. This is the Bill Pugh or initialization-on-demand holder idiom:

Holder.java
public class Printer {
    private Printer() {}

    private static class Holder {                  // not loaded until first touched
        static final Printer INSTANCE = new Printer();   // JVM: runs exactly once
    }

    public static Printer getInstance() {
        return Holder.INSTANCE;                    // no lock, no null check
    }
}

Holder isn't loaded when Printer is loaded — only when Holder.INSTANCE is first read. That read triggers class initialisation, the JVM builds the instance once under its own one-time lock, and every call afterwards is a plain field read. You get lazy (nothing is built until first use), thread-safe (guaranteed by the class loader), and fast (zero synchronisation in your code) — and the method body is one line. In the prototype's 🏅 Holder mode the lane is visibly the shortest of the four and 🔒 lock acquisitions stays at 0.

The simplest bulletproof Java Singleton is an enum

public enum Printer { INSTANCE; ... } gives you all of the above plus free protection against serialization and reflection attacks, because the JVM guarantees enum constants are unique. Joshua Bloch calls it the best way to implement a singleton in Effective Java. Its only real drawbacks: it's eager (created when the enum class loads) and it can't extend another class.

Eager initialisation — the honest simple option

You can also just write private static final Printer INSTANCE = new Printer(); directly on the class. The JVM's one-time class-initialisation guarantee makes this completely thread-safe with zero ceremony. The only thing you lose is laziness: the object is built the moment the class loads, whether or not anyone ever asks for it. That's fine for something cheap, and wasteful for something that opens sockets or reads files. The scoreboard row 🥱 Eager field in the prototype shows exactly that trade: 1 instance, 0 locks, lazy: ✗. The holder idiom exists to give you the same safety without giving up laziness.

Other languages have their own guarantees

  • Python — a plain module-level object is already a singleton, because a module's body executes exactly once on first import and the import machinery holds a lock while it does. If you must build one lazily inside a class, wrap the check-and-create in a threading.Lock — the GIL does not make check-then-act safe.
  • C++11 and later — a static local variable inside a function (a Meyers singleton) is guaranteed by the standard to be initialised exactly once, with other threads blocked until it's done. These are nicknamed magic statics. std::call_once with a std::once_flag is the explicit equivalent.
  • Gosync.Once is the idiom: once.Do(func() { instance = newPrinter() }) runs the function exactly once no matter how many goroutines call it, and guarantees the result is visible to all of them. Package-level var plus func init() is the eager equivalent.
  • C#Lazy<T> with the default thread-safety mode does the whole job for you, and a static readonly field gets the same one-time initialisation guarantee from the CLR.

Making the object immutable dissolves most of the problem

All of this is about safely publishing one object. If that object also holds mutable state that many threads read and write, you have a second, larger thread-safety problem inside it that no amount of clever getInstance() will fix. A singleton whose fields are all final and never change is the easiest kind to get right.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Two threads, 🧵 T1 and 🧵 T2, call getInstance() at the same instant. Pick an implementation with the chips — ❌ Naive, 🐢 synchronized, ⚡ DCL + volatile, 🏅 Holder — then hit ▶ Both call getInstance() and watch the two lanes step through their instructions in a fixed interleaving (or drive it yourself with ⏭ Step). Watch two counters: 🏭 instances created and 🔒 lock acquisitions. In Naive, both lanes pass the null check and the instance counter goes to 2 — two red printer boxes, one guarantee broken. In synchronized it stays at 1, but T2 visibly waits at the lock on every round and the lock counter climbs to 6. In DCL the lock is taken on the first call only, then the counter stops rising — and flipping the volatile: ON/OFF switch makes T2 grab a ⚠️ half-built object. In Holder there is no lock and no null check at all — the shortest lane of the four, with 🔒 0. The scoreboard at the bottom keeps every verdict side by side.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Break it on purpose

Leave the chip on ❌ Naive and press ▶ Both call getInstance(). Watch the lanes: T1 reads the field and sees null, then T2 reads it and also sees null — neither has written yet. Both run instance = new Printer(), 🏭 instances created jumps to 2, and two printer boxes appear (the second in red). Each thread's 'got:' line shows a different object. That is the race condition, made visible in six steps.

try 02

Feel the cost of the lock, then remove it

Switch to 🐢 synchronized and run all three rounds. Instances stay at 1 ✓ — but T2 turns amber and sits at '⏸ waiting for lock' on every single round, and 🔒 lock acquisitions climbs 2 → 4 → 6 long after the instance exists. Now switch to ⚡ DCL + volatile and run three rounds again: the lock counter moves during round 1 and then stops dead, because rounds 2 and 3 exit on the lock-free first check. Use ⏭ Step to walk round 1 slowly and watch T2 stop at the second check inside the lock.

try 03

Turn volatile off, then meet the winner

Still in ⚡ DCL mode, click the volatile: ON switch so it reads volatile: OFF, then run round 1. T1 publishes the reference before the constructor finishes; T2's first check sees non-null, skips the lock, and its lane flashes red with '⚠️ got a bad object' next to a ⚠️ half-built printer. Finally switch to 🏅 Holder: two instructions, no lock badge, no null check, 🔒 lock acquisitions: 0, and instances 1 ✓. Compare all five rows in the scoreboard — including 🥱 Eager field, the one that scores lazy: ✗.

In practice

When to use it — and what trips people up

Pick the implementation like this

  • Default: the static holder idiom (or enum in Java, a module-level object in Python, a function-local static in C++, sync.Once in Go). Lazy, thread-safe, lock-free, and short enough to get right on the first try.
  • Eager static final field — when the object is cheap to build and certain to be used. Simplest possible correct code; you're only paying startup cost you were going to pay anyway.
  • Double-checked locking — when you genuinely need lazy creation and the holder idiom doesn't fit, typically because the instance depends on runtime arguments or lives in a non-static field. Write volatile. Never skip it.
  • Plain synchronized method — fine when getInstance() is called rarely, or in a quick prototype. Correct is better than clever; optimise only if the lock actually shows up in a profile.

Step back before you optimise the lock

The best fix is often to not need a lazy global at all. Create the one instance at startup and pass it to whatever needs it — dependency injection gives you "exactly one" without any of this, and it stays testable. See Dependency Injection & IoC. If you do keep a singleton, remember that thread-safe creation is only half the job: the object's own mutable state still needs its own locking or immutability, as covered by Singleton's trade-offs.

What it gives you

  • The holder idiom gives you lazy, thread-safe, lock-free initialisation in one line — the language runtime does the hard part and can't be got wrong.
  • Double-checked locking removes the lock from the common path entirely: after the first call, getInstance() is a single field read.
  • A one-element enum (Java) adds free protection against serialization and reflection creating a second instance.
  • Eager initialisation is trivially correct — no locks, no checks, nothing to review — whenever laziness isn't worth anything.

Common mistakes

  • Naive lazy initialisation fails intermittently and invisibly: it usually works, so the duplicate-instance bug reaches production and appears only under load.
  • A synchronized getInstance() taxes every call forever, including the millions that happen after the instance already exists.
  • Double-checked locking is famously easy to write incorrectly — forget volatile and you publish half-built objects, and no test will reliably catch it.
  • The holder idiom can't take runtime arguments and only works for a static instance, so it doesn't cover every case.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

import java.util.*;

/** All four implementations side by side, worst to best. */
public class Printer {

    private final List<String> queue = new ArrayList<>();
    private Printer() { }                       // 🔒 no 'new Printer()' outside

    // ── 1. NAIVE — broken under concurrency ────────────────────────────
    private static Printer naive;
    public static Printer naiveGet() {
        if (naive == null) {                    // two threads can BOTH pass here
            naive = new Printer();              // → two instances
        }
        return naive;
    }

    // ── 2. SYNCHRONIZED — correct, but every call pays for the lock ────
    private static Printer synced;
    public static synchronized Printer syncedGet() {
        if (synced == null) synced = new Printer();
        return synced;                          // lock acquired on EVERY call
    }

    // ── 3. DOUBLE-CHECKED LOCKING — needs volatile, or it is broken ────
    private static volatile Printer dcl;        // volatile: no reordering,
                                                // no stale reads
    public static Printer dclGet() {
        if (dcl == null) {                      // ① cheap check, no lock
            synchronized (Printer.class) {      // ② only first callers get here
                if (dcl == null) {              // ③ check again under the lock
                    dcl = new Printer();
                }
            }
        }
        return dcl;                             // later calls skip the lock
    }

    // ── 4. STATIC HOLDER (Bill Pugh) — lazy, lock-free, usually the answer
    private static class Holder {               // loaded on first touch only
        static final Printer INSTANCE = new Printer();  // JVM runs this ONCE
    }
    public static Printer getInstance() {
        return Holder.INSTANCE;                 // no lock, no null check
    }

    public void print(String job) { queue.add(job); }
}

// ── 5. The simplest bulletproof form: a one-element enum ───────────────
// enum PrinterEnum {
//     INSTANCE;                                 // unique even across
//     public void print(String job) { ... }      // serialization + reflection
// }

References & further reading

7 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 06

Why is if (instance == null) instance = new Printer(); unsafe with two threads?

question 02 / 06

What is the real drawback of putting synchronized on the whole getInstance() method?

question 03 / 06

In double-checked locking, what does the SECOND null check accomplish?

question 04 / 06

What goes wrong if the DCL field is not declared volatile?

question 05 / 06

Why is the static holder idiom (a private nested class holding the instance) usually the preferred answer?

question 06 / 06

Which pairing of language and idiomatic one-time initialisation is correct?

0/6 answered