118. Double-Checked Locking Trap and Broken Idioms
Double-checked locking without the volatile keyword is broken due to instruction reordering; understand the CPU memory barrier mechanics and why the Initialization-on-Demand Holder idiom is cleaner.
Explanation
The double-checked locking idiom was historically invented to avoid the synchronization overhead of obtaining a lazy singleton: ``java // BROKEN IDIOM: if (instance == null) { synchronized (Lock.class) { if (instance == null) { instance = new Helper(); // TRAP! } } } ` Why is this broken without volatile? Constructing an object (new Helper()) involves three bytecode steps: 1. Allocate memory. 2. Invoke the <init> constructor. 3. Assign the memory address to the reference instance`.
The JVM JIT compiler and hardware CPU caches are allowed to reorder steps 2 and 3! If step 3 executes before step 2 finishes, another thread checking if (instance == null) observes a non-null reference pointing to a partially constructed, uninitialized object! When that second thread calls methods on instance, it reads corrupted or default field values.
The Fix: 1. Mark the field private static volatile Helper instance;. Under the Java 5+ Memory Model, writes to volatile have a write-barrier that prevents instruction reordering with preceding memory writes. 2. Even better: Use the Initialization-on-Demand Holder idiom or an enum, letting JVM class loading guarantee thread-safe lazy initialization with zero synchronization cost!
Example
// BROKEN IDIOM:
if (instance == null) {
synchronized (Lock.class) {
if (instance == null) {
instance = new Helper(); // TRAP!
}
}
}Sources
- https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html โ The "Double-Checked Locking is Broken" Declaration
- https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom โ Initialization-on-Demand Holder Idiom