Two threads run the very same code — and see different values of the same variable. This is not a compiler error and not a runtime bug: it's how modern hardware works. The Java Memory Model (JMM) is a set of rules that defines when a write in one thread becomes visible to another.
Why Threads Can See "Stale" Data
A modern processor does not go to main memory on every operation — that's far too slow. Each core has its own cache (L1, L2), and a thread works precisely with it.
Imagine the following situation:
- Thread A writes
running = falseinto its cache. - Thread B reads
running— but from its own cache, wheretruestill sits. - The flag has changed, but thread B knows nothing about it.
Besides caches there is one more problem — reordering. The compiler and the processor may change the order of operations for the sake of optimization, as long as it doesn't break the behavior within a single thread. For a single thread the result is the same, but another thread may see the operations in an unexpected order.
// Thread A
object = new SomeObject(); // (1) allocate memory, (2) write fields, (3) assign the reference
ready = true;
// Thread B (without synchronization)
if (ready) {
object.doWork(); // object may not be fully initialized yet!
}
It is precisely for such cases that the JMM exists.
happens-before: The Visibility Guarantee
happens-before is a relationship between operations: if operation X happens-before operation Y, then all writes made in X are guaranteed to be visible to Y.
This does not mean that X executes earlier by the clock. It is a guarantee of visibility: the JVM is obliged to ensure that Y sees the up-to-date data.
A short formula: happens-before = "you will see everything I did up to this point."
The JMM establishes several built-in happens-before rules:
- A write to a
volatilevariable happens-before its subsequent read in any thread. - Exiting a
synchronizedblock happens-before entering the same monitor from another thread. Thread.start()happens-before any operations inside the started thread.- All operations of a thread happen-before a
Thread.join()from another thread. - An object's constructor happens-before the finalizer of that object.
If there is no happens-before relationship between two operations, there is no visibility guarantee either. A program may work correctly on your machine and break on another one with a different processor architecture.
volatile: What It Guarantees and What It Doesn't
The keyword volatile tells the JVM: "don't cache this variable, read and write it directly to shared memory, and establish happens-before on every access."
The classic example is a thread stop flag:
public class Worker implements Runnable {
private volatile boolean running = true; // visibility is guaranteed
public void stop() {
running = false; // the write is visible to other threads
}
@Override
public void run() {
while (running) { // read the up-to-date value each time
doWork();
}
}
}
Without volatile, the JVM is allowed to read running into a register once and never touch memory again. The thread would never see the change.
What volatile does not guarantee — atomicity of compound operations.
The increment counter++ is three operations: read, add one, write. volatile makes each of them visible, but does not make the whole triple indivisible:
private volatile int counter = 0;
// In two threads at the same time:
counter++; // UNSAFE: between the read and the write another thread may interfere
For an atomic increment you need AtomicInteger or synchronized. volatile is only for cases where one thread writes and the others only read, or when you need a visibility guarantee for flags and single assignments.
final Fields: Safe Publication
final fields of an object provide a special JMM guarantee: values written to final fields in the constructor are visible to any thread that obtains a reference to the object — even without explicit synchronization.
public class Config {
public final String host;
public final int port;
public Config(String host, int port) {
this.host = host; // guaranteed to be visible after the constructor
this.port = port;
}
}
This works only if the reference to the object does not "escape" from the constructor before it completes. Passing this to external code inside a constructor is a common mistake that breaks this guarantee.
Immutable objects are thread-safe precisely because all their fields are final and initialized in the constructor.
"Works on My Machine": Why It's Dangerous
Visibility problems almost never reproduce in debug mode. The reasons:
- The x86 architecture (most laptops) has a stricter memory model than the JMM requires. Many visibility bugs only show up on ARM or PowerPC.
- The JIT compiler in interpreter mode (the first runs) does not perform aggressive optimizations — after warm-up the behavior changes.
- The debugger inserts breakpoints that create memory barriers, hiding the races.
Code without explicit happens-before guarantees is technically incorrect, even if it has worked for years without visible errors.
In Short
- Threads work with CPU caches, not directly with main memory — one thread's writes are not required to become visible to another immediately.
- The compiler and the processor reorder operations — for a single thread everything is correct, but another thread may see a different order.
- happens-before is a relationship that guarantees visibility: everything done before a happens-before event is visible to everyone who comes after it.
volatileguarantees visibility and happens-before on every access, but not the atomicity of compound operations such as an increment.finalfields are safely published without explicit synchronization — provided thatthisdoes not escape from the constructor.- "Works on my machine" does not mean correctness: a different architecture or a warmed-up JIT may reveal the bug.
What to Read Next
- Race Conditions — what exactly happens when happens-before is broken, and how it looks in practice.
- synchronized and Monitors — how synchronized blocks create happens-before and protect compound operations.
- Atomic Operations —
AtomicInteger,AtomicReference, and CAS as a replacement forvolatilewhere you need atomicity.