Back to Safe Publication & Memory Model
Q4Question 4 of 5

What is instruction reordering?

Key Concepts
  • Instruction reordering by the compiler (javac), JIT (HotSpot C2), and CPU (out-of-order execution) can change the order of operations for performance while preserving single-thread correctness.
  • The 'new Object()' operation has three steps: allocate memory, run constructor, assign reference. The CPU or JIT may reorder to: allocate, assign reference, then run constructor.
  • Another thread can see a non-null reference to an object whose constructor has not completed, reading uninitialized fields.
  • This is the root cause of the broken double-checked locking pattern without volatile.
  • Volatile prevents reordering: a volatile write inserts a STORE-STORE barrier before it and a STORE-LOAD barrier after it.
  • x86 CPUs have a strong memory model where most reorderings do not happen, but ARM and POWER CPUs have weak models where barriers are essential.
  • Safe alternatives to DCL: enum singleton (JVM guarantees thread-safe initialization), static holder pattern (class loading guarantee), or Spring @Bean.

Definition

The compiler, JIT, and CPU can reorder instructions for performance, and for 'new Object()' this means another thread may see a non-null reference to an object whose constructor has not finished, which is why DCL requires volatile.
  • Instruction reordering – three levels of optimization can change instruction order: the Java compiler (javac), the JIT compiler (HotSpot C2), and the CPU hardware (out-of-order execution, store buffers). All three preserve as-if-serial semantics for the executing thread but make no guarantees for other threads.
  • DCL breakage – instance = new Singleton() compiles to three bytecode steps: (1) allocate memory (NEW), (2) invoke constructor (INVOKESPECIAL), (3) assign reference (PUTSTATIC). The CPU or JIT may reorder to 1, 3, 2. After step 3, instance is non-null. Before step 2, the constructor has not run and fields are uninitialized. Another thread checking instance != null proceeds with a half-constructed object.
  • Volatile fix – a volatile write inserts a STORE-STORE barrier: all prior stores must complete before the volatile store is visible. This ensures the constructor (step 2) completes before the reference assignment (step 3) is visible to other threads.
  • Memory barriers – on x86, only STORE-LOAD requires an explicit MFENCE instruction (other barriers are free due to the strong memory model). On ARM and POWER, all barriers require explicit instructions, making volatile more expensive.

Think of opening a new branch of National Bank of India. The three steps are: (1) buy the building, (2) install the vault, counters, and computers, (3) put up the "OPEN" sign. Without volatile, the CPU might reorder to: buy building, put up the sign, then install the vault. A customer sees the "OPEN" sign and walks in, but there is no vault, no counters, and no computers. The branch is half-built. Volatile is the rule that says: never put up the sign until everything is installed.

Understanding the Concepts

What is instruction reordering?

Instruction reordering is an optimization performed at three levels. The Java compiler (javac) can reorder statements that have no data dependency. The JIT compiler (HotSpot C2) aggressively reorders instructions to fill CPU pipeline slots and eliminate stalls. The CPU hardware executes instructions out of order, speculatively, and uses store buffers to delay writes.

All three levels respect as-if-serial semantics for the thread that is executing the code. This means that within a single thread, the program behaves as if instructions ran in order, even though they may have been reordered internally. The problem arises when another thread observes the effects of the reordered instructions.

How does reordering break double-checked locking?

The expression instance = new Singleton() is not a single atomic operation. It compiles to three separate bytecode instructions.

JAVA
package com.bank.service;

public class BrokenDCL {
    private static BrokenDCL instance;  // NOT volatile: BROKEN
    private String connectionUrl;

    private BrokenDCL() {
        this.connectionUrl = "jdbc:oracle:thin:@nbi-prod:1521:BANKDB";
    }

    public static BrokenDCL getInstance() {
        if (instance == null) {                     // check 1 (no lock)
            synchronized (BrokenDCL.class) {
                if (instance == null) {              // check 2 (with lock)
                    instance = new BrokenDCL();
                    // Bytecode:
                    // 1. NEW BrokenDCL         → allocate memory
                    // 2. INVOKESPECIAL <init>   → run constructor
                    // 3. PUTSTATIC instance     → assign reference
                    // CPU may reorder to: 1 → 3 → 2
                    // After step 3: instance is non-null
                    // Before step 2: connectionUrl is null!
                }
            }
        }
        return instance;
    }
}
// Thread-A executes steps 1 → 3 → (has not done step 2 yet)
// Thread-B: if (instance == null) → FALSE! (assigned at step 3)
// Thread-B: return instance → connectionUrl is NULL → NPE!

How does volatile fix the reordering?

JAVA
package com.bank.service;

public class SafeDCL {
    private static volatile SafeDCL instance;  // VOLATILE: prevents reordering
    private String connectionUrl;

    private SafeDCL() {
        this.connectionUrl = "jdbc:oracle:thin:@nbi-prod:1521:BANKDB";
    }

    public static SafeDCL getInstance() {
        if (instance == null) {
            synchronized (SafeDCL.class) {
                if (instance == null) {
                    instance = new SafeDCL();
                    // Volatile write inserts STORE-STORE barrier BEFORE it:
                    // All stores (constructor field writes) MUST complete
                    // BEFORE the volatile store (reference assignment) is visible.
                    // Step 2 is guaranteed to complete before step 3 is visible.
                }
            }
        }
        return instance;  // volatile read: always sees fully constructed object
    }
}

What memory barriers does volatile insert?

A volatile write inserts two barriers. A STORE-STORE barrier before the volatile write ensures that all prior stores are flushed to memory before the volatile store. A STORE-LOAD barrier after the volatile write ensures that the volatile store is visible before any subsequent loads. A volatile read inserts a LOAD-LOAD barrier (this load completes before subsequent loads) and a LOAD-STORE barrier (this load completes before subsequent stores).

| Barrier | What it prevents | When inserted |
|---|---|---|
| STORE-STORE | Prior stores from moving past the volatile store | Before volatile write |
| STORE-LOAD | The volatile store from moving past subsequent loads | After volatile write |
| LOAD-LOAD | Prior loads from moving past this load | After volatile read |
| LOAD-STORE | Prior loads from moving past subsequent stores | After volatile read |

On x86 (Intel/AMD), the STORE-STORE, LOAD-LOAD, and LOAD-STORE barriers are free because x86 has a strong memory model that does not reorder these pairs. Only STORE-LOAD requires an explicit MFENCE instruction. On ARM and POWER, all four barriers require explicit CPU instructions, making volatile writes more expensive.

What are safe alternatives to DCL?

The enum singleton pattern is the simplest and safest alternative. The JVM guarantees that enum constants are initialized exactly once, in a thread-safe manner, during class loading.

JAVA
package com.bank.service;

public enum ConnectionPool {
    INSTANCE;  // thread-safe, serialization-safe, reflection-safe

    private final String url = "jdbc:oracle:thin:@nbi-prod:1521:BANKDB";

    public String getUrl() {
        return url;
    }
}
// Usage: ConnectionPool.INSTANCE.getUrl()

The static holder pattern uses the class loading guarantee. A static inner class is not loaded until it is referenced, and class loading is synchronized by the JVM.

JAVA
package com.bank.service;

public class ConnectionPoolHolder {
    private ConnectionPoolHolder() { }

    private static class Holder {
        static final ConnectionPoolHolder INSTANCE = new ConnectionPoolHolder();
    }

    public static ConnectionPoolHolder getInstance() {
        return Holder.INSTANCE;  // Holder class loaded on first call
        // JVM guarantees thread-safe class initialization
    }
}

What are common mistakes with reordering?

The most common mistake is assuming that because x86 has a strong memory model, reordering bugs will not occur in production. The JIT compiler reorders instructions independently of the CPU, and JIT reordering happens on every architecture. A program that "works" on x86 without volatile may fail when the JIT optimizes more aggressively or when deployed on ARM-based cloud instances.

What should a senior engineer know?

Senior engineers should understand that volatile is not free. On ARM (used in AWS Graviton instances), a volatile write inserts a DMB ISH (Data Memory Barrier, Inner Shareable) instruction that costs 10-50 nanoseconds. In tight loops with millions of iterations, this overhead adds up. For framework authors, VarHandle with acquire/release semantics provides the same correctness at lower cost on these architectures.

What are common interview follow-up questions?

  • Can you use final fields instead of volatile for DCL? If all fields are final, the final field freeze guarantees visibility after the constructor completes. However, the reference to the outer object still needs proper publication (volatile or synchronized).
  • Is reordering deterministic? No. It depends on JIT compilation thresholds, CPU load, and cache state. This makes reordering bugs extremely hard to reproduce in testing.

Visual Overview

DIAGRAM
  Instruction Reordering and DCL
  ══════════════════════════════

  instance = new Singleton();

  Normal order:              Reordered (CPU/JIT):
  1. allocate memory         1. allocate memory
  2. run constructor         3. assign reference ← non-null!
  3. assign reference        2. run constructor  ← fields null!

  Thread-B sees:
  instance != null → TRUE (step 3 done)
  instance.field → NULL (step 2 not done!)

  Volatile fix:
  1. allocate memory
  2. run constructor
  ─── STORE-STORE barrier ───
  3. volatile write: assign reference
  → Step 2 guaranteed before step 3 is visible

Key Takeaway

Instruction reordering at three levels (compiler, JIT, and CPU) can break multi-threaded code while preserving single-threaded correctness. The most dangerous case is instance = new Singleton(), which the CPU or JIT may reorder to: allocate memory, assign the reference (now non-null), then run the constructor (fields still uninitialized). Another thread checking instance != null proceeds with a half-constructed object and gets NullPointerException on field access.

Volatile prevents this by inserting a STORE-STORE barrier before the volatile write, guaranteeing that all prior stores (constructor field initialization) complete before the reference assignment is visible. Safe alternatives avoid the problem entirely: enum singleton (JVM guarantees thread-safe initialization), static holder pattern (class loading guarantee), or Spring managed beans.

At National Bank of India, a HikariCP connection pool singleton without volatile on the instance field caused a once-per-month NullPointerException when getConnection() was called by a thread that saw a non-null reference to an incompletely constructed pool. Adding volatile to the field eliminated the bug permanently.

Remember one sentence: the CPU can reorder new Object() to publish the reference before the constructor runs, and volatile prevents this by inserting a memory barrier.

---

Share
Use ← → arrow keys to navigate