Safeguard
Application Security•

CWE-609 Double-Checked Locking: Why the Classic Pattern Fails

Double-checked locking tries to skip synchronization, but other threads may see half-built objects. What goes wrong in Java and the safe ways to do lazy initialization.

Safeguard Security Team
3 min read

CWE-609 describes double-checked locking used to access a resource without the cost of explicit synchronization, where the locking turns out to be insufficient. MITRE lists Java as the affected language.

How it happens

The pattern checks whether a resource is initialized, takes a lock, checks again, and then initializes it if needed. The aim is to avoid locking on every access.

// Vulnerable pattern (Java before 1.5, or without volatile)
class Holder {
    private static Helper helper;

    static Helper get() {
        if (helper == null) {                    // first check, no lock
            synchronized (Holder.class) {
                if (helper == null) {            // second check
                    helper = new Helper();       // may be seen half-built
                }
            }
        }
        return helper;
    }
}

The record explains why this is not safe. It is not guaranteed to work in all languages and on all architectures. Other threads may not be operating inside the synchronized block, and are not guaranteed to see the operations execute in the same order as they appear inside it. A thread can see a non-null reference to an object whose fields are not yet set.

What an attacker gains

MITRE lists integrity impact: application data modified and execution logic altered. These bugs are intermittent and hard to reproduce. Under load, an attacker who can drive many concurrent requests may raise the odds of hitting the window, with effects that range from wrong data to broken security objects, such as a half-configured security component.

How to find it

Static analysis (SAST) can find some instances by recognising the double check shape. Code review should flag any lazy initialization that reads a shared field outside a lock.

How to fix it

MITRE's guidance for Java:

  • Double-checked locking is inherently flawed in Java before 1.5. Before that, only the synchronized keyword is known to work.
  • Beginning in Java 1.5, the volatile keyword allows double-checked locking to work, although there is debate about whether it gains enough performance.
private static volatile Helper helper;   // volatile makes the pattern correct

Often the simplest option is better: synchronize the accessor, or let the class loader do the work with a holder class, or initialize eagerly.

class Holder {
    private static class Lazy { static final Helper INSTANCE = new Helper(); }
    static Helper get() { return Lazy.INSTANCE; }
}

Quick check

What does volatile change from Java 1.5 onward? It lets double-checked locking work correctly, because writes become visible in order to other threads.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.