What Actually Happens Inside Your CPU When Two Java Threads Share a Variable
Running multithreaded software in modern multi-core processor is a physical coordination challenge between the CPU cores, local caches (L1, L2 and L3) and main memory(RAM). Here is what happens inside the machine when threads executes simultaneously.
Why CPU caches exist ?
A single CPU core can execute billions of instructions per second. Main memory RAM is physically far away on the motherboard and accessing it is slow compared to the speed the CPU process, below is sample estimate of execution time
CPU Core Cycle: ~ 0.3 nanoseconds
L1 Cache Access: ~ 1 nanosecond
Main RAM Access: ~ 50-100 nanoseconds
Now, if every thread fetched variable values from the RAM (which is far away) multi-core CPU would spend 90% of its time sitting idle waiting for the data signals received from the RAM.
To bridge this gap, there are ultra-fast memory stores exists in the CPU chip called L1, L2 and shared L3.

When Thread A running on Core 1 reads a variable, the CPU copies that variable from RAM into Core 1's local L1/L2 cache. Subsequent reads and writes happen inside that ultra-fast local cache.
Problem #1: The Illusion of Immediate Visibility
boolean ready = false;
// Thread A (Core 1)
while (!ready) { ... }
// Thread B (Core 2)
ready = true;You might expect Thread A to exit immediately when Thread B writes true. In practice, it can loop forever. Why?
JIT Compiler Hoisting: The JIT compiler optimizes loops aggressively. Because
readyis not markedvolatile, it assumes no other thread modifies it within this loop. It hoists the read out of the loop and caches it in a register, effectively compiling your code intowhile (true).Store Buffers: At the silicon level, Core 2 doesn’t write directly to shared memory instantly. Writes sit in a local hardware store buffer to keep the pipeline moving, meaning Core 1 may not see the update immediately.

The Fix: Marking ready as volatile. This does two things: it stops the JIT compiler from hoisting the check into a register, and it issues a hardware fence that flushes Core 2's store buffer. That way, the update doesn't sit in a local queue it becomes visible to other cores immediately.
Problem #2: The Atomicity Trap
At a high-level incrementing a number looks like a single operation
count++;But at the hardware level, count++ splits into three distinct hardware cycles:
LOAD: Read the value of
countfrom main memory into a CPU register.MODIFY: Add 1 to the register value.
STORE: Write the register value back to main memory.
If Core 1 and Core 2 execute this sequence concurrently when count = 10, both may load 10, compute 11, and overwrite the same value. Two increments run, but the counter only increases by 1 which results in race condition.

Important: Adding volatile does NOT fix this. volatile guarantees visibility, not atomicity for compound operations.
The Fix:
Mutual Exclusion:
synchronizedblocks orReentrantLockensure only one thread enters the critical section.Hardware CAS:
AtomicInteger/AtomicReferenceleverage low-level Compare-And-Swap instructions to perform atomic read-modify-writes without heavy locks.
At the end of the day, safe multithreading isn’t about forcing your CPU to crawl back to slow RAM on every single line. It’s about setting the right guardrails. Tools like volatile, atomic classes, and locks don't disable hardware optimizations they just tell the CPU and compiler when to stop reordering things and ensure every core is looking at the same state.