Traps

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.

Java 21 (JMM volatile requirement)

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