1.Introduction
2.When does reordering happen in Java
3.About memory models
4.Java Memory Model(JMM) guarantees
5.Summary
Take a look at the code below and try to guess all the possible values for x and y when it runs with multiple threads(method thread1() would be run with Thread 1 and method thread2() with Thread 2). This is Oracle Java (version 21):
int x, y;
int r1, r2;
public void thread1() {
x = r2;
r1 = 1;
}
public void thread2() {
y = r1;
r2 = 1;
}
Here's a hint: there are four possible outcomes for the pair (x, y): (0, 0), (0, 1), (1, 0), (1, 1). You can run this example with e.g. jcstress library https://github.com/openjdk/jcstress Test example
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;
@JCStressTest
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Reordering happened")
@Outcome(id = "", expect = Expect.ACCEPTABLE, desc = "Sequential execution")
@State
public class JMMReordering {
int x, y;
int r1, r2;
@Actor
public void thread1() {
x = r2;
r1 = 1;
}
@Actor
public void thread2() {
y = r1;
r2 = 1;
}
@Arbiter
public void result(II_Result r) {
r.r1 = x;
r.r2 = y;
}
}
You might think:
"Okay, (0,1), (1,0), and (0,0) make sense — they can happen because of data races. But where on earth can the result (1, 1) come from?"
The short answer is: the Java Memory Model (JMM) — specifically, due to reordering.
The JMM may instruct your JVM(compiler, JIT compiler) to reorder 'r2 = 1' before 'y = r1' and/or 'x = r2' after 'r1 = 1' for the sake of optimization. This isn't unusual — in fact, the JMM does this all the time.
You can think about JMM as additional program running behind the scene to determine all possible behaviors for threads in every program written on Java. It is part of the JVM.
Importantly, these reorderings don’t change the outcome of a single-threaded program. But in multi-threaded code, they can lead to unexpected results — like (1,1) in that case.
WHEN DOES REORDERING HAPPEN IN JAVA?
You might now ask:
“When should I expect instruction reordering in my Java program?”
There can be a lot of different instructions for reordering. The good news is, you usually don’t need to know exactly where reordering will happen. In most cases, Java developers don't have to worry about low-level optimizations. But if you're working with multi-threaded code, then understanding how to avoid unpredictable behavior is essential. The key is: your program must be correctly synchronized — and we’ll explain what that means.
Reordering depends on how the Java Memory Model (JMM) is implemented. Different JVM vendors (like Oracle, Amazon Corretto, etc.) may apply their own optimizations. These optimizations often include instruction reordering for better performance. To better understand how reordering and other transformations occur in your specific JVM version, it's important to consult that JVM's official documentation.
It's often easier to think about what prevents reordering, instead of trying to predict when it will happen. Here's a (not exhaustive) list of things that prevent reordering:
1. Happens-before edges which brings happens-before relationship for actions in code.
If there is a happens-before edges(or memory barriers) between two actions, the JVM cannot reorder them. Some key examples:
- Volatile variables Volatile variables create half memory barriers: All reads and writes before write to volatile variable in code will never be reordered to occur after write to volatile variable. All reads and writes after read of volatile variable will never occur before the read of volatile variable.
This creates what’s called a release-acquire pair. For example, if r1 and r2 in the earlier example above were declared volatile, then the instructions involving them would not be reordered — and the result (1, 1) would never occur.
Why not use volatile on x and y instead? That could help avoid (1,1) too — but it wouldn't solve the visibility problem for r1 and r2. Still, it’s not a major issue in this small example.
- Circular data dependency (circular causality) Java prohibits out-of-thin-air values. That means if variables in code depend on each other in a circular way, the JVM won’t make up values that magically satisfy the dependency.
Example — this code will never result in r1 == 42 or r2 == 42:
int x, y;
int r1, r2;
public void thread1() {
r1 = x;
if (r1 != 0)
y = 42;
}
public void thread2() {
r2 = y;
if (r2 != 0)
x = 42;
}
Here, y = 42 only happens if r1 != 0, which requires x == 42, which depends on r2 != 0, which depends on y == 42 again. It’s a loop — a circular dependency. The JMM avoids reordering in such cases because it would lead to results that are impossible in single-threaded execution(intra-thread semantic) in Java.
2. Infinite loops
If a thread is stuck in an infinite loop, any actions after the loop will not be moved before it.
volatile int x, y;
int r1, r2;
public void thread1() {
do {
r1 = x;
} while (r1 == 0);
y = 42;
}
public void thread2() {
do {
r2 = y;
} while (r2 == 0);
x = 42;
}
Here, both threads will loop forever, and because the loops may never exit, the JVM can’t reorder actions before the loops.
3. Conflicting accesses
The access is write to or read of variable. Two accesses to the same shared variable or array element are said to be conflicting if at least one of the accesses is a write. Reordering is not allowed if there is conflicting accesses to the same shared variable.
For example:
int x, y;
int r1, r2;
public void thread1() {
r1 = x;
if (r1 != 0)
y = 42;
}
public void thread2() {
r2 = y;
if (r2 != 0)
x = 42;
}
Here, in method thread1, there's both a read and a write to r1. It is conflicting accesses. The JVM won’t reorder r1 = x to after the if block because that would break the logic. The same is for 'thread2' and 'r2'.
4. Synchronization actions and external actions
Certain operations are never reordered:
Synchronization actions, such as
-Reading/writing a volatile variable
-Locking and unlocking monitors (e.g., synchronized blocks)
-Thread start and join (Thread.start(), Thread.join())
-The start and end of a thread (even if not represented by a method)
External actions, like
-Printing to the console
-Reading/writing files
-Network operations, etc.
For example, the following code will always print "Hello" before "World":
public void thread1() {
System.out.println("Hello");
System.out.println("World");
}
Printing is external to the program’s internal state, it's observable — and must respect order.
5. Single-thread logic(intra-thread semantic) prevents reordering
Even without synchronization, if reordering would change the behavior in a single-threaded program and JMM detects it, then it’s not allowed.
This is part of what makes reasoning about Java code easier: actions within the same thread will appear to happen in the order they are written, as long as that’s required for correct behavior.
ABOUT MEMORY MODELS
Compilers, virtual machines, and even processors often optimize your code to make it run faster on specific hardware. These optimizations involve transformations like instruction reordering, removing redundant synchronization (for example, when two variables point to the same underlying value), speculative execution (which may lead to so-called "out-of-thin-air" values), and more. The Java Memory Model (JMM) decides which transformations can safely be applied to your Java program.
The concept of a memory model isn’t unique to Java. Every programming language that supports multithreading needs a memory model — a set of rules that define how operations in different threads interact through memory. Memory models are typically designed to be both easy for developers to use and flexible for system designers. But what do we mean by "easy to use" and "flexible"?
Imagine a multithreaded Java program where no unexpected behavior could ever occur — no data races, no reordering, and everything always runs exactly in the order it appears in the code. You wouldn’t have to worry about using volatile, synchronized, AtomicInteger, or any of the usual concurrency tools. The JMM would automatically apply the correct synchronization, and your program would behave exactly as written. This would definitely make it easier to write multithreaded code — and that’s what we mean by "easy to use."
However, such a memory model comes with serious downsides. First, it’s difficult to implement correctly. (Although some models like this do exist) More importantly, this kind of strict ordering — called sequential consistency — would prevent most performance optimizations, making your programs significantly slower.
Now, consider the opposite extreme: a model that allows all possible optimizations (including unsafe ones like out-of-thin-air values). In this case, the code could be extremely fast, but tools like volatile, synchronized, or AtomicInteger might not be enough to ensure correct behavior. You’d have performance, but at the cost of predictability and correctness.
In this context, "flexibility" refers to how many transformations the memory model allows. The more it allows, the more flexible (and potentially faster) the model is — but also harder for developers to reason about.
That’s why the JMM tries to strike a balance. It aims to allow useful optimizations without making the programmer's life too difficult. In other words, it tries to balance simplicity for developers with flexibility for system designers — making everyone as happy as possible.
JMM GUARANTEES
The Java Memory Model (JMM) provides two main guarantees (which are also its core requirements):
1. Sequentially consistent execution for correctly synchronized programs
If your code is correctly synchronized (e.g., using synchronize block, volatile, AtomicInteger, etc.), it will execute in a predictable and consistent order — just like it appears in your code. There will be no data races, and the program will behave exactly as you expect.
2. Clear behavior for incorrectly synchronized programs
If the program is not correctly synchronized, then issues like data races, reordering, and other inconsistencies may occur — but not all possible transformations will be allowed. JMM still restricts the worst behaviors.
The first guarantee is especially important for developers because it tells us how to write multithreaded programs that behave correctly.
The second guarantee is more about how the memory model internally manages optimizations — and it varies across different memory models. We won’t go into those technical details here.(again refer to your specific version JVM's documentation for further details about this behavior)
Two Key Parts of the Java Memory Model
There are two main parts of the JMM that help ensure sequentially consistent behavior in correctly synchronized programs:
- Happens-Before Consistency
- Causality Requirements
Let’s look at them more closely.
1. Happens-Before Consistency
This is a simplified way to understand the behavior of the JMM. It describes how actions (like reads and writes) must relate to one another to be considered correct.
Key rules of the happens-before model
Synchronization Order:
There is a specific order in which synchronization actions (like locking and unlocking) must happen. For example, in a synchronized block, the lock must happen before the unlock.
This creates a “happens-before” relationship. The JMM guarantees that this relationship is preserved. These synchronization points are known as synchronization actions (you can see the full list in JLS §17.4.2).
Intra-thread Consistency:
For a single thread, all actions will happen in the order they appear in code — just like in a regular, single-threaded program.
The JMM guarantees that optimizations will not break this order. So, you’ll always see values that make sense based on the written code.
Happens-before consistency (non-volatile):
For normal (non-volatile) variables, the JMM ensures that a read will see a write that happened before it, as long as the program respects the happens-before rules.
This helps prevent strange behaviors like reading a value that hasn’t been written yet (e.g. “out-of-thin-air” values).
If your code is correctly synchronized, this problem will not occur.
Synchronization order consistency:
For volatile variables, there is a special rule: a read will always see the last write to the same variable (according to the synchronization order).
This works similarly to the happens-before rule, but is specific to volatile fields.
In short if every read in your program sees a write that happened before it (in terms of execution, not necessarily source code), your program is considered happens-before consistent.
But here's a tricky part, the phrase “happened before” refers to the actual execution order, not the source code order. So, even if something appears later in the code, it may run earlier due to reordering.
For example:
int a = 0;
a = 2;
int b = a;
You might expect b to be 2, but it’s possible for a = 2 to be executed after b = a due to reordering. So, b could be 0.
Even though it’s not what you expected, it still follows happens-before rules because 'a' had a default value of 0 written before and read action would see that 0.
That said, the happens-before model does not prevent some problematic behaviors — especially out-of-thin-air values. That’s where the second part - causality requirements - comes in.
2. Causality Requirements
Causality is a more complex part of the memory model and mostly relevant to language designers and compiler writers — not everyday developers.
Let’s try to explain it simply.
The causality requirements are rules the JMM uses to decide if a certain execution is legal. Each program execution can be broken into a set of actions: reads, writes, locks, unlocks, etc. The JMM checks the order of these actions and whether they make sense based on its rules.
If the execution order doesn’t break any rules, it’s - allowed.
If it violates causality rules, the JMM may reorder the actions or reject the execution. If no valid ordering is possible, it could result in a runtime or compile-time error (in theory).
You don’t need to know every detail of causality unless you’re building your own memory model.
Just remember this simple idea. Causality requirements prevent illegal executions where a read sees a write that never actually happened before it in program order.
Summary
The JMM provides a strong guarantee:
If your program is correctly synchronized, it will behave in a sequentially consistent way.
That means:
Every read will see the last write.
The order of actions is predictable and consistent.
Java provides many tools to help ensure correct synchronization, like:
synchronized blocks
volatile variables
atomic classes from java.util.concurrent.atomic and more
These tools help your program follow the happens-before order, and when used properly, your program will run safely and predictably.
The rest — causality, reorderings, out-of-thin-air values — are mostly concerns for JVM designers and compiler engineers.
Top comments (0)