Double-Checked Locking in Java — The Pattern, the Bug, and the Fix
Fast recall
Quick revision
Core answer
volatile. For singletons specifically, `enum` is simplest and safest. DCL still appears in interviews, library code, and real production performance-sensitive paths, but for singletons it's rarely the right modern choice.Whether the candidate understands why DCL was broken pre-JMM (reordering of construction and reference assignment), why volatile fixes it, and can articulate modern alternatives (holder idiom, enum)
Writing DCL without volatile — still broken on JVMs across all Java versions. Thinking synchronized alone is enough. Using DCL for singletons when the holder idiom or enum is simpler
Explain the reordering concretely (object ref visible before fields are initialized), mention that volatile became sufficient in Java 5's JMM, and that the holder idiom leverages JVM class-initialization locking for free
Interview-ready guide
Interview answer
Double-checked locking is a lazy-initialization optimization with a famous historical bug.
The pattern
- Check
instance == nullwithout locking (fast path) - If null, acquire lock, check again, then construct
- Goal: avoid locking on every call after the instance exists
Why it was broken pre-Java 5
instance = new Singleton()is not atomic- JVM can reorder: allocate → assign reference → run constructor
- Or: allocate → run constructor → assign reference
- Before Java 5's JMM, either order was legal
- Another thread doing the unsynchronized first check could see a non-null reference with uninitialized fields
- Intermittent NPEs / garbage data — the classic production-only bug
The Java 5+ fix — volatile
- Declare
private static volatile Singleton instance; volatileestablishes happens-before between the write toinstanceand any read- Constructor's field writes are guaranteed visible to anyone reading the reference
- Reordering no longer allowed
- Use a local variable inside the method to avoid re-reading volatile (performance)
Without volatile, DCL is still broken on Java 21
- The memory model guarantee is specifically tied to volatile semantics
- No amount of synchronized keyword on one side fixes the unsynchronized read
Better alternatives for singletons
1. Initialization-on-demand holder idiom — most senior Java engineers' favorite:
- Private static nested class holding the instance
- JVM class initialization is thread-safe and lazy
- No volatile, no synchronized, no boilerplate
- Downside: can't pass constructor arguments
2. Enum singleton — Joshua Bloch's recommended default:
public enum Singleton { INSTANCE; }- Thread-safe, serialization-safe, reflection-safe
- Zero boilerplate
- Can't subclass, can't easily refactor out of singleton
When DCL is still correct
- Lazy instance fields (not statics) where construction needs
this.config - Hand-rolling
ConcurrentHashMap.computeIfAbsentfor per-key caching - Guava's `Suppliers.memoize` encapsulates DCL as a library utility — prefer over hand-rolling
Decision guide
- Simple singleton, no args → enum
- Simple singleton, want class-based → holder idiom
- Lazy instance field, per-instance args → DCL with volatile
- Per-key cached value → ConcurrentHashMap.computeIfAbsent
Performance reality check
- DCL's advantage over plain
synchronized getInstance()is real but only matters at millions-per-second call rates - For 99% of application code,
synchronizedis fine - Don't DCL-ify code prematurely
Recommendation: For modern Java singletons, default to enum for the simplest case or initialization-on-demand holder idiom when you need a class-based form — both are simpler and safer than DCL. Reserve DCL with volatile for lazy instance fields that genuinely need constructor arguments and are on a hot enough path to justify the complexity. Never write DCL without volatile — it's still broken in Java 21. And when in doubt, use library helpers like Guava's `Suppliers.memoize` so the pattern lives in one well-tested place rather than scattered across your codebase.
Double-Checked Locking in Java — The Pattern, the Bug, and the Fix
Direct Answer
Double-checked locking (DCL) is an optimization for lazy initialization: check if the instance is null (fast, no lock), if yes, acquire the lock and check again (now safely), and only then create the instance. The idea was to avoid the cost of acquiring a lock on every read. Pre-Java 5 it was broken due to the lack of a formal Java Memory Model: the write of the instance's fields could be reordered with the write of the reference itself, letting another thread see a partially-constructed object via the unsynchronized first check. The fix (Java 5+): declare the instance field volatile — that establishes the happens-before edge preventing reordering. The better alternative: use the initialization-on-demand holder idiom (a private static nested class) — it's thread-safe, lazy, and requires no synchronization or volatile. For singletons specifically, enum is simplest and safest. DCL still appears in interviews, library code, and real production performance-sensitive paths, but for singletons it's rarely the right modern choice.
Common Mistakes & Pitfalls
Writing DCL without volatile — still broken on JVMs across all Java versions. Thinking synchronized alone is enough. Using DCL for singletons when the holder idiom or enum is simpler
Check your understanding
Answer these without looking back at the article.
- Why specifically does `volatile` fix DCL? What guarantee does it provide?
- If you're on Java 21, do you still need `volatile` in DCL?
- Why is the holder idiom thread-safe without any `synchronized` or `volatile`?
- How would `AtomicReference.compareAndSet` replace DCL?
- Why does `enum` solve the singleton-serialization problem that class-based singletons have?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.