Fundamentals 10 min read

Why Intuition Fails: Ordering, Instruction Reordering, and Volatile in Java Concurrency

The article explains how Java's memory model and compiler optimizations can reorder writes, causing ordering bugs in multithreaded programs, demonstrates the issue with simple and jcstress tests, and shows that declaring the flag as volatile restores the expected behavior.

JD Tech
JD Tech
JD Tech
Why Intuition Fails: Ordering, Instruction Reordering, and Volatile in Java Concurrency

Concurrency is notoriously difficult because programmers often rely on intuition that the execution order of statements matches the source order, which is not guaranteed on modern CPUs and JVMs.

A simple example creates two threads: thread T1 writes data = 666 and then isReady = true ; thread T2 spins on while (!isReady) {} and then reads r = data + 222 . Intuitively one expects r == 888 , but due to possible reordering the result 222 can appear.

boolean isReady = false;
int data = 0;
int r;
public void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        Thread t1 = new Thread(() -> {
            data = 666;
            isReady = true;
        });
        Thread t2 = new Thread(() -> {
            while (!isReady) {};
            r = data + 222;
        });
        t2.start();
        t1.start();
        t2.join();
        if (r != 888) {
            System.out.println(r);
        }
    }
}

Running the program many times often shows no abnormal result, but deeper analysis reveals that the JVM, JIT compiler, or CPU may reorder the writes, making isReady become visible before data is updated.

To expose such subtle bugs, the article uses the OpenJDK jcstress framework, which runs the same scenario under a controlled stress test and records all possible outcomes.

@JCStressTest
@Outcome(id = "888", expect = Expect.ACCEPTABLE, desc = "expected")
@Outcome(id = "0",   expect = Expect.ACCEPTABLE, desc = "expected")
@Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "abnormal")
@State
public class IsReadyTest {
    int data = 0;
    boolean isReady = false;

    @Actor
    void actor1() {
        data = 666;
        isReady = true;
    }

    @Actor
    void actor2(I_Result r) {
        if (!isReady) {
            r.r1 = 0;
        } else {
            r.r1 = data + 222;
        }
    }
}

The test reports both the expected result 888 and the unexpected result 222 , the latter indicating that isReady became true while data was still zero – a classic manifestation of instruction reordering.

Replacing the if check with a while loop (busy‑wait) reproduces the problem more dramatically, because the compiler may hoist the read of isReady out of the loop, turning the loop into an infinite spin.

@JCStressTest
@Outcome(id = "888", expect = Expect.ACCEPTABLE, desc = "expected")
@Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "abnormal")
@State
public class IsReadyTestError {
    int data = 0;
    boolean isReady = false;
    @Actor
    void actor1() {
        data = 666;
        isReady = true;
    }
    @Actor
    void actor2(I_Result r) {
        while (!isReady) {};
        r.r1 = data + 222;
    }
}

The root cause is instruction reordering: the JVM/JIT is allowed to reorder independent writes for performance, and the CPU may also execute memory operations out of program order. Without proper synchronization, other threads can observe a later write before an earlier one.

The straightforward fix is to declare the flag isReady as volatile . A volatile write establishes a happens‑before relationship with subsequent volatile reads, preventing the write to data from being reordered after the write to isReady and ensuring visibility across threads.

int data = 0;
volatile boolean isReady = false;

// writer thread
data = 666;
isReady = true;

// reader thread
while (!isReady) {};
int r = data + 222; // r will always be 888

In addition to ordering, volatile also disables certain CPU cache optimizations, guaranteeing that each read sees the most recent write.

In summary, writing correct concurrent Java code requires understanding of the Java Memory Model, the effects of instruction reordering, and the proper use of synchronization primitives such as volatile , locks, or higher‑level concurrent utilities.

JavaConcurrencyvolatileMemory Modelinstruction reorderingjcstress
JD Tech
Written by

JD Tech

Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.