Question 5 · Section 2

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)

Language versions: English Russian Ukrainian

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:

  1. First thread: instance = null -> synchronized -> creates
  2. Second thread: instance = null -> waits for lock -> sees it’s already created
  3. 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

  1. You don’t understand JMM — use Bill Pugh Singleton
  2. Bill Pugh is sufficient — simpler and more reliable
  3. 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

  1. DCL in Java < 5 didn’t work
    • Before JSR-133 (2004) the Memory Model was broken
    • volatile didn’t guarantee happens-before
  2. Forgot volatile
    • Compiles, works in tests
    • In production -> random bugs
  3. 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

  1. volatile is MANDATORY in DCL
  2. Local variable optimization for performance
  3. Bill Pugh preferred over DCL for Singleton
  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)

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