What is Double-Checked Locking?
4. VarHandle (Java 9+) for extreme performance 5. Don't use DCL without understanding JMM 6. DCL is useful for lazy caching (not just Singleton)
Junior Level
Double-Checked Locking (DCL) is an optimization of the synchronized approach. Instead of blocking on EVERY call, we block only during the first object creation.
Problem: Ordinary synchronized adds ~10-50ns per call. In a hot path with millions of calls, this amounts to seconds of overhead.
Solution: Check twice — first without locking, then with locking.
Example:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
// 1st check (fast)
if (instance == null) {
// Lock only if creation is needed
synchronized (Singleton.class) {
// 2nd check (safe)
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
How it works:
- First thread: instance = null -> synchronized -> creates
- Second thread: instance = null -> waits for lock -> sees it’s already created
- Third thread: instance != null -> returns immediately (no synchronized!)
Why volatile: Without it, another thread may get a partially created object.
When NOT to use DCL
- You don’t understand JMM — use Bill Pugh Singleton
- Bill Pugh is sufficient — simpler and more reliable
- Not a hot path — synchronized method is fast enough
Middle Level
Why two checks?
// One check = slow
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// synchronized is called EVERY time!
// DCL = fast after initialization
public static Singleton getInstance() {
if (instance == null) { // Fast check
synchronized (Singleton.class) {
if (instance == null) { // Precise check
instance = new Singleton();
}
}
}
return instance;
}
// synchronized only during creation!
Why is volatile mandatory?
// Without volatile — DANGEROUS!
private static Singleton instance;
// instance = new Singleton() consists of:
// 1. Allocate memory
// 2. Call constructor
// 3. Assign reference to instance
// Processor may reorder: 1 -> 3 -> 2
// Thread B sees instance != null (step 3)
// But constructor not executed (step 2)!
// -> NullPointerException or corrupted state
// With volatile — safe
private static volatile Singleton instance;
// volatile prevents reordering!
Local Variable Optimization
public static Singleton getInstance() {
Singleton local = instance; // Read volatile once
if (local == null) {
synchronized (Singleton.class) {
local = instance;
if (local == null) {
instance = local = new Singleton();
}
}
}
return local;
}
// Without optimization: instance read 2-3 times (volatile = slow)
// With optimization: instance read 1 time -> ~25% faster
Approach Comparison
| Approach | Speed | Safety | Complexity |
|---|---|---|---|
| Synchronized | Slow | Yes | Simple |
| DCL without volatile | Fast | No | Medium |
| DCL + volatile | Fast | Yes | High |
| Bill Pugh | Fast | Yes | Simple |
| Enum | Fast | Yes | Minimal |
Senior Level
JSR-133 — Java Memory Model specification (2004), guaranteeing correct volatile behavior. Instruction Reordering — processor/compiler reorders instructions for optimization.
Java Memory Model Deep Dive
Instruction Reordering:
Without volatile, JIT/processor may do:
Thread A:
1. obj = allocate() // Memory allocated, fields = null/0
2. instance = obj // Reference published!
3. invokeConstructor(obj) // Constructor executes
Thread B (interferes after step 2):
if (instance != null) {
instance.doWork(); // Object not yet initialized!
}
Memory Barriers from volatile:
Volatile write inserts:
StoreStore barrier — all writes BEFORE volatile complete
StoreLoad barrier — all writes AFTER volatile see fresh data
Volatile read inserts:
LoadLoad barrier — all reads AFTER volatile see fresh data
LoadStore barrier — all writes AFTER volatile won't be reordered
Happens-Before:
Write to volatile happens-before read from volatile
-> All writes in constructor are VISIBLE to the thread reading instance
VarHandle Alternative (Java 9+)
public class VarHandleDCL {
private static final VarHandle HANDLE = MethodHandles.lookup()
.findStaticVarHandle(VarHandleDCL.class, "instance", VarHandleDCL.class);
private static VarHandleDCL instance;
public static VarHandleDCL getInstance() {
// getAcquire — weaker than volatile read
VarHandleDCL local = (VarHandleDCL) HANDLE.getAcquire();
if (local == null) {
synchronized (VarHandleDCL.class) {
local = (VarHandleDCL) HANDLE.getAcquire();
if (local == null) {
local = new VarHandleDCL();
// setRelease — weaker than volatile write
HANDLE.setRelease(null, local);
}
}
}
return local;
}
}
// getAcquire/setRelease = Ordered Access
// Cheaper than volatile, but sufficient for DCL
DCL is not only for Singleton
// Caching expensive computations
public class ExpensiveCalculator {
private volatile int cachedHash;
private volatile boolean hashComputed;
public int hashCode() {
if (!hashComputed) {
synchronized (this) {
if (!hashComputed) {
cachedHash = computeExpensiveHash();
hashComputed = true;
}
}
}
return cachedHash;
}
}
// String.hashCode() uses the same principle!
Performance Benchmark
10M getInstance() calls:
Synchronized: 150ms (lock every time)
DCL without volatile: 12ms (fast, but broken!)
DCL + volatile: 18ms (volatile read overhead)
DCL + volatile + local: 15ms (optimization)
Bill Pugh: 12ms (class loading magic)
VarHandle DCL: 13ms (weaker barriers)
Conclusion: Bill Pugh — best balance of simplicity and speed
Common Pitfalls
- DCL in Java < 5 didn’t work
- Before JSR-133 (2004) the Memory Model was broken
- volatile didn’t guarantee happens-before
- Forgot volatile
- Compiles, works in tests
- In production -> random bugs
- Incorrect local variable usage
// Meaningless if (instance == null) { Singleton local = new Singleton(); synchronized (...) { instance = local; } } // Correct Singleton local = instance; // Read FIRST if (local == null) { ... }
Production Experience
Real scenario: DCL bug after 3 years of operation
- Application worked without issues
- Migration to new server (different CPU) -> NPE
- Cause: ARM processor -> different reordering
- Solution: added volatile -> problem disappeared
- Lesson: DCL without volatile = undefined behavior
Best Practices
- volatile is MANDATORY in DCL
- Local variable optimization for performance
- Bill Pugh preferred over DCL for Singleton
- VarHandle (Java 9+) for extreme performance
- Don’t use DCL without understanding JMM
- DCL is useful for lazy caching (not just Singleton)
Senior Summary
- DCL = optimization for lazy initialization
- volatile is critical — prevents Instruction Reordering
- Memory Barriers: StoreStore/LoadLoad ensure happens-before
- Local variable: reduces volatile reads by 25%
- VarHandle: weaker barriers -> faster than volatile
- Before Java 5 DCL didn’t work (broken Memory Model)
- String.hashCode() uses DCL principle
- Without volatile — undefined behavior, especially on ARM
Interview Cheat Sheet
Must know:
- DCL — optimization: lock only during object creation, not on every call
- volatile is MANDATORY — prevents instruction reordering (memory -> publish -> constructor)
- Memory Barriers: StoreStore/LoadLoad provide happens-before guarantee
- Local variable optimization reduces volatile reads by 25%
- Before Java 5 (JSR-133) DCL didn’t work — Memory Model was broken
- DCL is used not only for Singleton, but also for lazy caching
- VarHandle (Java 9+): getAcquire/setRelease — cheaper than volatile
Common follow-up questions:
- What happens without volatile? — Reordering: thread sees reference before constructor executes -> NPE or corrupted state
- Why two checks? — First (without sync) for speed, second (in sync) for thread-safety
- DCL only for Singleton? — No, for any lazy caching (example: String.hashCode())
- Why is Bill Pugh preferred over DCL? — Simpler, more reliable, doesn’t require volatile, JIT optimizes
Red flags (DO NOT say):
- “DCL without volatile works — I checked” — undefined behavior, manifests on different CPU/JDK
- “Synchronized on method is fast enough” — 150ms vs 15ms on 10M calls
- “I don’t use volatile, I have no problems” — bug may appear when migrating to different CPU
- “DCL is only for Singleton” — used for any lazy initialization
Related topics:
- [[04. How to implement thread-safe Singleton]] — DCL as an implementation approach
- [[03. What is Singleton]] — general Singleton context
- [[06. What are problems with Singleton]] — Singleton problems
- [[02. What pattern categories exist]] — Creational patterns
- [[16. What anti-patterns do you know]] — design anti-patterns