skip to content

A Singleton creates its instance lazily on first call to its accessor. What can go wrong when several threads call that accessor at the same time, and which implementation strategies fix it?

level: middleimportance: must knowfreq 74%

answer

  1. check-then-act race → two instances
  2. unsafe publication → half-built object visible
  3. holder idiom = lazy + free + safe
  4. DCL needs volatile/atomic, else broken
  5. prefer call_once / sync.Once / Lazy over hand-rolled

basics

~20 s

Two threads can both see the field as empty and each create an instance, so you end up with two. Fix it by creating the instance eagerly at class load, or by guarding the check-and-create with a lock or a run-once primitive.

solid answer

~50 s

Naive lazy initialization (`if (instance == null) instance = new T()`) is a check-then-act race: two threads can pass the null check before either assigns, producing two instances and possibly two sets of side effects. A second, subtler failure is unsafe publication — one thread may see a non-null reference to an object whose fields are not yet visible, because the compiler/CPU may reorder the assignment ahead of the constructor's writes. Fixes, cheapest first: eager/static initialization, where the runtime's class-initialization lock guarantees once-only, safely-published construction; the initialization-on-demand holder idiom, which is eager inside a nested type so it stays lazy until first use with zero synchronization; a run-once primitive (`call_once`, `sync.Once`, `Lazy`); or double-checked locking with the field marked volatile/atomic so publication is ordered. Plain double-checked locking without the memory barrier is a classic broken idiom.

code

pseudocode · 22 lines
pseudocode
// BROKEN: race + unsafe publication
static getInstance() {
    if (instance == null) instance = new Heavy();
    return instance;
}

// CORRECT (a): lazy holder — runtime guarantees once-only class init
class Holder { static final Heavy INSTANCE = new Heavy(); }
static getInstance() { return Holder.INSTANCE; }

// CORRECT (b): double-checked locking — ONLY with an ordered field
volatile static Heavy instance;
static getInstance() {
    Heavy local = instance;                 // one read of the ordered field
    if (local == null) {
        lock (LOCK) {
            local = instance;
            if (local == null) { local = new Heavy(); instance = local; }
        }
    }
    return local;
}

go deeper

for a junior

Name the race: two threads both see the field empty and both construct. Say that eager creation or a lock around the check-and-create fixes it.

for a middle

Add the second failure — unsafe publication of a partially constructed object — and name at least two safe idioms (eager static, holder, run-once primitive, correct DCL).

for a senior

Explain why DCL needs an ordered field in memory-model terms, compare fast-path costs, and discuss failure-during-initialization semantics and reentrancy deadlock.

for a principal

Argue for the boring option: use the platform's run-once primitive, ban hand-rolled DCL in review, and note that once-only side effects (port binding, migrations) need coordination beyond in-process locking anyway.

## Two distinct bugs hide in one line of code ``` static getInstance(): if (instance == null) # (A) check instance = new Heavy() # (B) create + assign return instance ``` ### Bug 1 — the check-then-act race (duplicate instances) Thread T1 evaluates (A) and finds `null`. Before T1 reaches (B), the scheduler switches to T2, which also evaluates (A), also finds `null`. Both proceed to construct. You now have **two** instances; the second assignment wins, so the two threads hold *different* objects. Consequences range from harmless (a stateless helper) to severe: two caches that diverge, two connection pools each opening the configured maximum, two ID generators emitting overlapping IDs, a setup side effect (file created, port bound, native library initialised) executed twice. ### Bug 2 — unsafe publication (a half-built object) Even if only one thread constructs, another thread may observe a **non-null reference to an incompletely initialised object**. Constructing an object involves allocating memory, running the constructor's field writes, then storing the reference. Compilers and CPUs are permitted to reorder the reference store *before* the field writes become visible to other threads, as long as the reordering is invisible to the *constructing* thread. A reader that only checks `instance != null` can therefore see the reference and then read default/garbage field values. This is a **memory-model** problem, not a scheduling problem — no amount of retrying makes it go away, and it can be invisible on a strongly-ordered CPU and then appear in production on a weakly-ordered one. ## Strategy 1 — eager (static) initialization Build the instance when the class/module is initialised: ``` class Registry: private static final instance = new Registry() ``` Language runtimes that have a class-initialization phase (JVM, .NET) run it **under an internal lock, exactly once, with a memory barrier at the end**, so both bugs are impossible with no code from you. Cost: the object is built whether or not it is used, at a time you do not fully control, and a failure during construction surfaces as an obscure class-initialization error. ## Strategy 2 — initialization-on-demand holder ``` class Registry: private static class Holder: static final instance = new Registry() static getInstance(): return Holder.instance ``` The nested holder type is only initialised when it is first *referenced*, i.e. on the first `getInstance()` call. You get laziness with the runtime's once-only, safely-published class-initialization guarantee and **zero synchronization on the fast path**. On the JVM this is the textbook answer. It requires a language with lazy, thread-safe type initialization. ## Strategy 3 — a run-once primitive Most modern standard libraries ship one: - `std::call_once` / function-local `static` ("magic statics") in C++11+ - `sync.Once` in Go - `Lazy<T>` (with the thread-safe mode) in .NET - `lazy { }` / `object` in Kotlin, `lazy_static`/`OnceLock` in Rust They encapsulate the lock, guarantee the initialiser runs exactly once, and publish safely. **Prefer these over hand-rolled locking** — they are correct by construction and usually optimised (fast path is a single atomic load). ## Strategy 4 — synchronize the whole accessor ``` static synchronized getInstance(): if (instance == null) instance = new Registry() return instance ``` Correct and trivially reviewable. The downside is that **every** call — not just the first — pays lock acquisition, and under high contention the accessor becomes a serialization point. On modern runtimes with biased/thin locks this is far cheaper than folklore suggests, so measure before rejecting it. ## Strategy 5 — double-checked locking (DCL) ``` static getInstance(): if (instance == null) { # fast path, no lock lock (classLock) { if (instance == null) # re-check under the lock instance = new Registry() } } return instance ``` The *only* thing that makes this correct is declaring the field with the language's ordering annotation — `volatile` in Java/C#, an atomic with acquire/release ordering in C++ — plus reading it into a **local variable** to avoid re-reading. Without that, the outer unlocked read is exactly bug 2. DCL was famously **broken on the JVM before Java 5**, and remains broken in any language whose memory model does not order the store. Modern verdict: DCL is a correct-but-fiddly micro-optimisation; the holder idiom or a run-once primitive is preferred, and reviewers should treat a hand-written DCL as a smell. ## Comparison | Strategy | Lazy? | Fast-path cost | Failure mode if done wrong | |---|---|---|---| | Eager static | No | None | Startup cost, obscure init errors | | Holder idiom | Yes | None | Needs runtime with lazy type init | | Run-once primitive | Yes | One atomic load | Choosing a non-thread-safe mode | | Synchronized accessor | Yes | Lock every call | Contention, not correctness | | Double-checked locking | Yes | One volatile read | Torn/half-built object if the field isn't volatile | ## Edge cases worth naming - **Reentrancy / initialization deadlock**: if the constructor (directly or transitively) calls back into `getInstance()`, a synchronized accessor deadlocks or a holder-based one returns a partially built object. Keep constructors dependency-free. - **Exceptions during initialization**: with class-init-based approaches the type is marked erroneous and every later access fails with a different, confusing error; with a locked accessor the field stays null and the next caller retries — which may be better or worse depending on whether the failure is transient. - **Once-only side effects**: if construction binds a port or writes a file, a duplicate instance is not just wasteful, it is a hard failure. That is the case where getting this right actually matters. - **Uniqueness ends at the process boundary**: none of this prevents a second instance in a second process or a second classloader.

  • Why exactly does double-checked locking need the field marked volatile/atomic — isn't the lock enough?
    The lock protects the *writer*, but the first (fast-path) read happens outside the lock, so it has no ordering guarantee. Without an ordered field, the reference store may become visible before the constructor's writes, letting a reader observe a non-null but half-initialised object. The volatile/atomic release-store plus acquire-load supplies the missing ordering.
  • Is a synchronized accessor actually too slow in practice?
    Usually not. Modern runtimes make uncontended lock acquisition very cheap, and the accessor is rarely on a hot inner loop. Reach for the holder idiom or a run-once primitive because they are simpler and free, not because synchronization is assumed catastrophic — and if you do suspect contention, measure it.
  • What happens if construction of a lazily initialised singleton throws?
    With class-initialization-based approaches the type is permanently marked as failed, and every later access throws a different, confusing error rather than retrying. With a locked accessor the field stays unset and the next caller retries — appropriate for transient failures but potentially an infinite retry loop for permanent ones. Decide deliberately which you want.

Two people walk into an empty kitchen, each sees no coffee brewing, and each starts a pot — that's the check-then-act race. Worse, someone can see a full pot on the counter and pour a cup before the water has actually finished dripping through — that's unsafe publication: the label says 'ready' before the contents are.

saying these in an interview costs you the question

  • "Double-checked locking fixes the race, so it's correct" — without a volatile/atomic field it still permits a half-constructed object to be observed.
  • "Two threads can't really both pass the null check" — check-then-act is the textbook race; it is rare, not impossible, which makes it worse.
  • Claiming eager initialization is always wasteful; it is free of synchronization and often the right default for small objects.
  • Testing thread safety by running the accessor in a loop and seeing no failure — memory-visibility bugs are architecture- and JIT-dependent.
  • Calling back into `getInstance()` from the constructor, which deadlocks or exposes a partially built instance.
  • Assuming thread-safe lazy init also gives you uniqueness across processes, classloaders, or nodes.

context