Double-Checked Locking (DCL) checks if the instance exists before and after acquiring the lock. First check avoids the lock, second check ensures only one thread creates.
The Pattern
Check if instance is null (no lock). If null, acquire the lock. Check again inside the lock. If still null, create the instance. The second check catches the race where another thread created it while you were waiting for the lock.
Double-Checked Locking Flow
Why Two Checks?
First check (outside lock): Fast path. If instance already exists, return immediately. No lock acquired. This is the common case after initialization.
Second check (inside lock): Safety net. Between first check and acquiring lock, another thread might have created the instance. Without second check, you would overwrite it.
The Hidden Danger
This pattern is broken without memory barriers. Object construction is not atomic. A thread might see a non-null reference to a partially constructed object. It reads garbage data.
The fix: In Java, declare the instance as volatile. In C++, use std::atomic or std::call_once. The memory barrier ensures the object is fully constructed before the reference becomes visible.
Critical: Double-checked locking without proper memory ordering is broken. The compiler and CPU can reorder instructions. A non-null reference does not guarantee a fully constructed object.
💡 Key Takeaways
✓DCL checks twice: once without lock (fast path), once with lock (safety). First check handles 99.9% of calls without any locking.
✓Second check is essential. Between your first check and acquiring the lock, another thread could create the instance.
✓Without memory barriers, DCL is broken. You can see a non-null pointer to a partially constructed object.
✓In Java, the instance field MUST be volatile. In C++, use std::atomic or std::call_once. Memory ordering is not optional.
✓DCL was considered an anti-pattern for years because early implementations ignored memory ordering. Modern versions with proper barriers are safe.
📌 Examples
1Java DCL: private static volatile Singleton instance. The volatile keyword prevents seeing a partially constructed object.
2Broken DCL in Java pre-1.5: Even with synchronized, without volatile, threads could see uninitialized fields in the singleton.
3C++ DCL: Must use std::atomic<Singleton*> with proper memory_order. Raw pointer assignment is not safe across threads.