Concurrency
Double-checked locking: interview questions and how to answer them
Double-checked locking avoids taking a lock on every read only when the shared reference has safe publication semantics and construction cannot be observed halfway through.
Written and reviewed by Sahil Srivastav
What it actually is
The pattern reads a singleton reference without locking, locks only when it is absent, checks again, and constructs once. The first read is an optimisation; the second check is required because another thread may have initialised the value while this thread waited.
Without a volatile reference or equivalent memory barrier, a compiler or CPU may publish the reference before all constructor writes are visible. A reader can then observe a non-null but partially initialised object.
Why it matters in production
The bug is rare, timing-dependent, and often disappears under a debugger. It can corrupt caches, configuration, or security policy at process startup and is difficult to reproduce from logs.
The safer alternatives are language-supported lazy initialisation, a static holder, or eager construction. DCL is justified only when construction is expensive and lazy access matters.
How it works
First read
The unsynchronised read is legal only if the reference is volatile, atomic, or otherwise safely published. It decides whether the lock is worth taking.
Second read
The locked read prevents two initialisers from constructing independent values. Omitting it makes the lock useless.
Publication
Volatile release on the write and acquire on the read orders constructor writes before a reader sees the reference.
Lifecycle
Singleton initialisation is only half the problem; replacement, shutdown, and failed construction need explicit semantics too.
Implementing it
Prefer a runtime’s static initialisation or holder idiom. If using DCL, make the reference volatile and keep construction side-effect free.
Test publication under many threads, but rely on the memory model rather than hoping a stress test catches a missing barrier.
Do not use a singleton to hide a dependency; inject a stable instance and make its lifecycle visible.
private static volatile Client client;
static Client getClient() {
Client c = client;
if (c == null) {
synchronized (Holder.class) {
c = client;
if (c == null) client = c = new Client();
}
}
return c;
}Interview questions and how to answer them
Why are there two checks?
The first avoids the lock on the common path. The second confirms that a different thread did not initialise the value while this thread was waiting.
Why must the field be volatile?
It supplies visibility and ordering: a reader that sees the reference also sees the constructor’s writes. Without it, publication can be reordered.
What is the simpler alternative?
Use language-supported static initialisation or a holder class, which gives one-time thread-safe initialisation without hand-written locking.
Does volatile make construction atomic?
No. It safely publishes the completed reference; the lock or runtime initialisation protocol ensures only one construction occurs.
Answers that lose the round
- Leaving the reference non-volatile
- Omitting the second check
- Returning `this` from construction callbacks
- Using DCL when eager construction is adequate
- Confusing atomic reference access with safe initialisation of the object graph
- Ignoring reset and shutdown
FAQ
Is DCL always broken?
It is broken without the memory-model requirement. With a correctly volatile reference in a language whose memory model defines this pattern, it can be correct.
Can an immutable object still need volatile publication?
Yes. Immutability prevents later mutation; it does not by itself make an unsafely published reference visible.
Does a concurrent map replace the need for DCL?
It can provide atomic compute-or-insert semantics, often with clearer lifecycle behaviour.