JUC Concurrency Basics: Java Memory Model and Its Three Key Properties—Visibility, Ordering, Atomicity
This article explains the Java Memory Model, distinguishes main memory from thread-local memory, and demonstrates how visibility, ordering, and atomicity affect concurrent programs through code examples, volatile and synchronized keywords, double‑checked locking, and the Happens‑Before rules.
1. Introduction
In high‑traffic, high‑concurrency internet services, mastering concurrency techniques is essential for any programmer. This article reviews core concepts of Java concurrency, focusing on the Java Memory Model (JMM) and the three fundamental properties of concurrent execution.
2. Java Memory Model (JMM)
The JMM defines the abstract relationship between threads and main memory, describing how the JVM interacts with physical RAM. It is distinct from the JVM runtime data areas, which partition memory for objects, stacks, etc.
Before JDK 1.2, variables were always read from shared main memory. Modern JMM allows a thread to cache a variable in its own local memory (e.g., CPU registers), which can lead to inconsistencies when one thread updates a variable in main memory while another continues to read its stale cached copy.
Main Memory : Stores all objects created by any thread; shared among threads.
Local Memory : Each thread holds a private copy of shared variables; only the owning thread can access it.
Note : Do not confuse the JMM with JVM memory regions (runtime data area).
3. The Three Concurrency Properties
The speed gap between CPU, memory, and I/O devices creates a core contradiction (CPU ≫ memory ≫ disk). Hardware, OS, and compiler optimizations mitigate this gap, but the JMM introduces new challenges for developers.
CPU adds caches to narrow the CPU‑memory speed gap.
OS introduces processes and threads to multiplex CPU time and balance CPU‑I/O differences.
Compilers reorder instructions to make better use of caches.
3.1 Visibility
Visibility means that a change made by one thread to a shared variable becomes immediately observable by other threads. Because each thread may keep a cached copy, visibility can be lost. The classic i++ example shows this:
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int k = 0; k < 10000; k++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int k = 0; k < 10000; k++) {
i++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i=" + i);
}The expected result is 20000, but the actual result ranges between 10000 and 20000, demonstrating that i++ is not thread‑safe due to visibility problems. The volatile keyword can guarantee visibility of a variable across threads.
3.2 Atomicity
Marking i as volatile does not make i++ atomic. The increment operation expands to three CPU instructions: load, add, and store. Thread switches can occur between any of these steps, leading to lost updates.
Instruction 1: Load i from memory (or cache) – volatile ensures the loaded value is the latest.
Instruction 2: Perform +1 in the register.
Instruction 3: Store the result back to memory (or cache).
When a context switch happens after instruction 1, both threads may operate on the same stale value, producing a final result smaller than 2.
To achieve atomicity, synchronized (or other atomic constructs) must be used:
private static int i = 0;
private static synchronized void add() { i++; }
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int k = 0; k < 10000; k++) add(); });
Thread t2 = new Thread(() -> { for (int k = 0; k < 10000; k++) add(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("i=" + i);
}On 32‑bit machines, 64‑bit long updates also suffer from non‑atomicity because they require multiple instructions.
3.3 Ordering
Ordering means that statements appear to execute in the order written. Compilers may reorder independent statements for performance, which can introduce bugs. The classic double‑checked locking singleton illustrates this:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}The three steps of instance creation (allocate memory, initialize, assign reference) can be reordered to 1 → 3 → 2, causing another thread to see a non‑null but uninitialized object. Declaring uniqueInstance as volatile prevents this reordering.
4. Happens‑Before Rules
The following code demonstrates the basic idea of a Happens‑Before relationship:
int x = 0;
volatile boolean v = false;
public void write() { x = 10; v = true; }
public void read() { if (v) System.out.println("x=" + x); }
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
new Thread(() -> demo.write()).start();
new Thread(() -> demo.read()).start();
}Before Java 1.5, the read thread could observe v == true while still seeing x == 0 because the write to x might be cached. Starting with Java 1.5, the enhanced volatile semantics introduce a Happens‑Before rule that guarantees x = 10 is visible after v = true becomes visible.
4.1 Program Order Rule
Within a single thread, each action Happens‑Before every subsequent action.
4.2 Volatile Variable Rule
A write to a volatile variable Happens‑Before every subsequent read of that same variable.
4.3 Transitivity
Write x = 10 Happens‑Before write v = true.
Write v = true Happens‑Before read v.
Therefore, x = 10 Happens‑Before the read of v, making x visible.
4.4 Monitor Lock Rule
Unlocking a monitor (exiting a synchronized block) Happens‑Before any subsequent lock acquisition of the same monitor.
synchronized (this) {
// critical section
if (this.x < 12) {
this.x = 12;
}
}Thus, a thread that enters the block after another thread has exited will see the updated value.
4.5 Thread start() Rule
Calling Thread.start() on a child thread Happens‑Before any action in that child thread.
Thread B = new Thread(() -> { /* child work */ });
var = 77; // main thread writes before start
B.start();4.6 Thread join() Rule
When a thread joins another, all actions performed by the joined thread happen‑before the join returns, making their effects visible to the joining thread.
Thread B = new Thread(() -> { var = 66; });
B.start();
B.join(); // after this, main thread sees var == 66Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
