skip to content

What is double-checked locking, why was it broken before Java 5, and how does volatile fix it?

level: seniorimportance: should knowfreq 60%

answer

  1. Check without lock, then lock and check again
  2. new Foo() = allocate + construct + publish; steps can reorder
  3. Pre-Java-5: reader could see a half-built object
  4. JSR-133 volatile gives acquire/release + happens-before
  5. Field MUST be volatile or it's still broken
  6. Prefer holder idiom unless you need parameterization

basics

~20 s

Double-checked locking checks a field for null without a lock, and only locks and builds the object if it's still null. Before Java 5 it could hand back a half-built object because writes could be reordered. Marking the field volatile prevents that reordering and makes the fix work.

solid answer

~50 s

Double-checked locking (DCL) is a lazy-init optimization: you check the field once without a lock (fast path), and only if it's null do you enter a synchronized block and check again before constructing. The second check avoids building twice when two threads race. The problem before Java 5 was the memory model: writing `instance = new Foo()` is really 'allocate, run constructor, publish reference', and those steps could be **reordered** so the reference became visible before the object was fully constructed. Another thread on the lock-free path could then see a non-null but **half-initialized** object. The Java 5 memory model (JSR-133) fixed this by giving `volatile` proper acquire/release semantics: declaring the field `volatile` forbids that reordering and guarantees a thread reading the non-null reference also sees all the constructor's writes (a happens-before edge). With `volatile`, DCL is correct; without it, it's still broken. In modern code the holder idiom or a memoizing Supplier are usually preferred unless you need a parameterized or instance-field lazy value.

go deeper

for a junior

Can recognize the check-lock-check shape and state that the field must be volatile, even if hazy on why.

for a middle

Explains the second check prevents double construction and that volatile is required, with a basic grasp of reordering.

for a senior

Explains the pre-Java-5 bug precisely (publish reordered before construction + no visibility), and how JSR-133 volatile's acquire/release and happens-before fix it.

for a principal

Weighs DCL against holder/Supplier/computeIfAbsent by failure semantics, parameterization, and maintainability, and can reason about benign-race acceptability for immutable values.

## The goal We want a lazily-built, thread-safe value, but acquiring a lock on **every** read is wasteful once the value exists. **Double-checked locking (DCL)** tries to lock only on the rare path where the value still needs building. ## The shape ```java private volatile Foo instance; // volatile is essential (see below) public Foo getInstance() { Foo result = instance; // 1st check, no lock (fast path) if (result == null) { synchronized (this) { result = instance; // 2nd check, under lock if (result == null) { result = new Foo(); instance = result; // publish } } } return result; } ``` - **First check** (no lock): if `instance` is already set, return it immediately — no synchronization cost on the common path. - **Lock + second check**: only threads that saw `null` enter the `synchronized` block. Inside, they re-check because another thread may have built it while they waited for the lock. Hence *double*-checked. - The local `result` variable is a minor optimization that reads the volatile field once. ## Why it was broken before Java 5 To understand the bug you need two ideas: 1. **`new Foo()` is not atomic.** It compiles to roughly: (a) allocate memory, (b) run the constructor to initialize fields, (c) assign the reference to `instance`. 2. **Reordering.** The Java compiler, JIT, and CPU are allowed to reorder operations as long as a **single thread** can't tell. Nothing stops step (c) — publishing the reference — from being moved **before** step (b) finishes. So a thread building the object could publish a **non-null reference to a not-yet-fully-constructed object**. A second thread on the lock-free fast path sees `instance != null`, skips the lock, and returns an object whose fields are still defaults/garbage. Worse, even with proper ordering, the old (pre-JSR-133) memory model gave **no visibility guarantee**: a reader might see the reference but stale field values. Because the bug is timing- and JIT-dependent, it passes tests and explodes in production. This is why DCL was famously declared 'broken' for years. ## How Java 5's memory model + volatile fixes it Java 5 adopted the **JSR-133** memory model, which strengthened `volatile`. A `volatile` field now has **acquire/release** semantics: - **Release on write**: when a thread writes the volatile `instance`, all memory writes it did **before** that (including the constructor's field initializations) are flushed and ordered **before** the volatile write. The publish can't 'jump ahead' of the construction. - **Acquire on read**: when another thread reads the non-null volatile `instance`, it is guaranteed to **also see everything written before** the release. This creates a **happens-before** edge between 'construct + publish' and 'read'. Result: any thread that sees a non-null `instance` sees a **fully constructed** object. `volatile` both forbids the harmful reordering and guarantees visibility. Without `volatile`, DCL is still wrong on Java 5+. ## Caveats and alternatives - The field **must** be `volatile` — a common interview trap is to write DCL without it. - DCL is verbose and easy to get subtly wrong. For a plain singleton, the **initialization-on-demand holder idiom** is simpler and equally correct. For per-key values, `ConcurrentHashMap.computeIfAbsent`; for a parameter-free deferred computation, a memoizing `Supplier`. DCL earns its keep mainly for **lazy instance fields** or values that need runtime parameters, where the holder idiom can't be used. - For primitives/immutable values you can sometimes drop the lock entirely; for an effectively-immutable object even a benign race (building twice) may be acceptable, but DCL with `volatile` is the safe default.

  • Exactly what could a second thread observe under broken DCL?
    It could see instance as non-null on the lock-free path and return it, but the object's fields are still at default/uninitialized values, because the reference was published (reordered) before the constructor finished, and the old memory model gave no visibility guarantee. The caller then uses a half-constructed object.
  • Is double-checked locking still needed in modern Java?
    Rarely for plain singletons — the holder idiom is simpler and safe. DCL remains useful for lazily initializing an instance field or a value that needs runtime parameters, where a static holder can't be used; there, DCL with a volatile field is correct on Java 5+.

saying these in an interview costs you the question

  • Writing DCL with a non-volatile field and calling it correct
  • Saying the bug is 'two objects get created' (the real bug is a half-constructed object becoming visible)
  • Claiming synchronized alone on the slow path fixes the fast-path visibility problem
  • Believing DCL was always fine, or is still broken on modern Java (it's correct with volatile since Java 5)

context