一、Java内存模型
Java 内存模型(Java Memory Model
,简称 JMM
)是 Java 并发编程的基石之一,是一种抽象规范并不是物理意义上的东西。它定义了:在多线程环境下,变量的读写在 JVM 中是如何进行的,以及各个线程之间如何保证可见性、有序性和原子性。
在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
.
1.1 为什么需要 Java 内存模型(JMM)?
在单线程程序中,代码按顺序执行,一切都很自然。但在 多线程程序中:
- 线程之间有自己的工作内存(缓存)。
- 如果不加控制,一个线程对变量的修改,另一个线程可能永远看不到(可见性问题)。
- 编译器/CPU 可能会进行 指令重排序,导致语义上的先后顺序被打乱(有序性问题)。
JMM 的作用就是屏蔽不同硬件和操作系统内存访问差异的细节,让 Java 程序在不同平台下都有一致的并发行为。
.
1.2 JMM把内存模型抽象为两部分:主内存和工作内存
- 主内存(Main Memory):
抽象层面是所有线程共享的内存区域,存储共享变量(实例字段、静态字段等);物理层面对应物理内存(RAM
) 或 JVM 堆内存。主内存是线程间数据交互的 “公共区域”,但直接读写主内存效率低(物理内存访问延迟约 100ns,远高于 CPU 缓存的 1-10ns) - 工作内存(Working Memory):
抽象层面是线程私有区域,存储主内存变量的副本;物理层面对应CPU 缓存(L1/L2/L3
) 和寄存器。线程对变量的所有操作(读、写)必须在工作内存中完成,再通过特定机制同步到主内存,这是为了利用 CPU 缓存提升效率,但也引入了一致性问题。 - 线程操作变量的过程:
- 主内存 <—> 工作内存 <—> 线程执行
注意:JMM ≠ 物理内存。JMM 是抽象模型,工作内存对应 CPU 缓存和寄存器,主内存对应物理 RAM。
.
1.3 内存交互的 8 种原子操作:从抽象到物理
JMM 定义的 8 种操作(lock
/unlock
/read
/load
/use
/assign
/store
/write
)并非直接对应 CPU 指令,而是对线程与内存交互的抽象描述,其物理映射如下:
关键特性:单个操作原子,但复合操作(如 i++ = read+load+use+assign+store+write
)不保证原子性,需通过 synchronized
或原子类实现。
.
1.4 happens-before原则的形式化语义与实践
happens-before 是 JMM 定义的 “逻辑时钟”,用于判断多线程操作的可见性与有序性,其核心是传递性与偏序关系
1.4.1 核心规则的底层映射
- 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作。
底层依赖编译器的 “as-if-serial
” 语义:允许重排序,但必须保证单线程执行结果不变(通过数据依赖分析避免破坏逻辑)。 - 监视器锁规则:解锁 happens-before 后续加锁。
底层对应 synchronized 的monitorenter
(加锁)和monitorexit
(解锁)指令:解锁时插入StoreStore
屏障刷新数据到主内存,加锁时插入LoadLoad
屏障从主内存加载最新值。 - volatile 规则:写操作 happens-before 后续读操作。
底层通过内存屏障实现:volatile 写后插入StoreLoad
屏障,读前插入LoadLoad
屏障,确保写操作的全局可见性与读操作的顺序性。 - 线程启动 / 终止规则:
start()
happens-before 线程内操作,线程内操作 happens-beforejoin()
返回。
底层依赖 JVM 对线程状态的同步:start()
会刷新主线程的共享变量到主内存,join()
会等待线程终止并加载其修改的变量。
1.4.2 传递性的典型应用
通过传递性可构建复杂的有序关系,例如
// 线程A
volatile int flag = 0;
int data = 0;
// 步骤1:A修改data
data = 100;
// 步骤2:A修改flag(volatile写)
flag = 1;
// 线程B
// 步骤3:B读取flag(volatile读)
if (flag == 1) {
// 步骤4:B读取data
System.out.println(data); // 必然输出100
}
- 步骤 1 happens-before 步骤 2(程序顺序规则);
- 步骤 2 happens-before 步骤 3(volatile 规则);
- 步骤 3 happens-before 步骤 4(程序顺序规则);
- 根据传递性:步骤 1 happens-before 步骤 4,故 data 的修改对 B 可见。
.
1.5 JMM在JVM层的具体实现是怎样的 - 内存屏障
JVM 本身并不直接实现 JMM,而是通过 插入特定的内存屏障(memory barrier
)指令,要求 CPU 遵守内存访问的某种顺序,达到 JMM 所定义的“可见性”和“有序性”。
Memory Barrier 是一种 CPU 指令,通常用于刷新本地缓存(保证主内存可见性)以及 控制指令重排序
.
1.5.1 常见的屏障指令
- LoadLoad Barrier:禁止读操作重排序,确保前面的加载操作先于后面的加载操作执行
- 如:
Load1;LoadLoad;Load2
。确保Load1
数据的装载先于Load2
及所有后续装载指令的装载
- 如:
- StoreStore Barrier:禁止写操作重排序,确保前面的写操作对主内存可见后,后续的写才能执行
- 如:
Store1;StoreStore;Store2
。确保Store1
数据对其他线程可见(刷新到内存)先于Store2
及所有后续存储指令的存储
- 如:
- LoadStore Barrier:禁止读操作与写操作重排序,确保前面的读完成后才能写
- 如:
Load1;LoadStore;Store2
。确保Load1
数据装载先于Store2
及所有后续的存储指令刷新到内存
- 如:
- StoreLoad Barrier:最强屏障,禁止所有重排序,刷新缓存,开销最大。确保前面的写对其他线程可见后,后续读才开始执行(一般用于
volatile
写之后和读之前)- 如:
Store;StoreLoad;Load2
。确保Store1
数据对其他处理器变得可见(指刷新到内存)先于Load2
及所有后续装载指令的装载。StoreLoad Barriers
会是该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令
- 如:
.
1.5.2 不同屏障的性能损耗
示例:一个 volatile
写操作在 x86 上的开销约为普通写的 10 倍(因 StoreLoad 屏障耗时),在 ARM 上更高(需额外指令保证顺序)
.
1.5.3 不同架构下的实现
1)JVM 的内存屏障最终需映射到具体硬件架构:
- x86 架构:
通过lock
前缀指令实现,如lock addl $0,0(%%esp)
,强制刷新缓存并锁定总线。- StoreLoad 屏障:通过
mfence
指令实现。 - 其他屏障:在 x86 中,部分屏障可通过内存访问顺序的自然约束隐式实现(如
LoadLoad
屏障)。
- StoreLoad 屏障:通过
- ARM 架构:使用
dmb
(数据内存屏障)和dsb
(数据同步屏障)指令。
2)不同架构之间应用上的差异
- x86 架构:因硬件天然保证
LoadLoad
、StoreStore
、LoadStore
的顺序(TSO 模型),JVM 可省略这些屏障,仅需实现StoreLoad
屏障; - ARM 架构:弱内存模型(允许更多重排序),JVM 需显式插入所有类型的屏障,故
volatile
操作在 ARM 上的开销是 x86 的 3 倍以上。
.
1.6 JMM 规定的三大特性
JMM 的核心目标是保证多线程环境下的原子性、可见性、有序性,其实现依赖硬件机制与软件规范的协同
1.6.1 原子性(Atomicity):从硬件锁到软件同步
原子性指操作不可分割(要么全执行,要么全不执行),比如保证 read
、load
、assign
、use
、store
、write
等单个操作是原子的,但复合操作(如 i++)不保证。还有通过 java.util.concurrent.atomic
包提供的原子类(如 AtomicInteger
)内部实现了复合操作的原子性
- 例如:
int a = 1;
是原子的 - 但
a++
并不是原子的(它其实是三步:读 -> 加 -> 写)
1)解决方法:
synchronized
、Lock
、AtomicInteger
等原子类。synchronized
和各种 Lock
可以让多线程访问串行化,保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。而各种原子类是利用 CAS
(compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作
2)底层实现原理:
那么上面的原子类各个如何实现,可以划分为两个层级:
- 硬件级原子性:
CPU 通过总线锁(Bus Lock
) 或缓存锁(Cache Lock
) 保证单个内存操作的原子性。例如,x86 的lock
前缀指令会锁定系统总线,阻止其他 CPU 访问该内存地址,确保操作原子性。 - 软件级原子性:
- synchronized:通过 JVM 的监视器锁(
monitor
)实现,本质是将代码块包装为lock
+ 操作 +unlock
,利用硬件锁保证原子性; - 原子类(如 AtomicInteger):基于
CAS
(Compare-And-Swap) 操作,映射到硬件的cmpxchg
指令(x86)或ldaxr
/stlxr
(ARM),通过循环重试实现复合操作的原子性; - Lock 接口:如
ReentrantLock
,底层通过AQS
(AbstractQueuedSynchronizer
)的CAS
操作 +volatile
变量实现原子性。
- synchronized:通过 JVM 的监视器锁(
.
1.6.2 可见性(Visibility):从缓存一致性到内存刷新
一个线程修改了变量的值,其他线程能马上看到修改后的值。问题的根源主要是:线程对变量的修改先在工作内存中进行,未及时刷新到主内存,导致其他线程无法立即看到最新值
// 线程1
flag = true;
// 线程2
while(!flag) {
// 可能一直等不到
}
1)解决方法:
- volatile 关键字:确保变量的修改立即刷新到主内存,并使其他线程的工作内存中该变量副本失效。
- synchronized 块:解锁前必须将变量刷新到主内存。
- final 关键字:初始化后不可变,保证可见性。
- Lock,例如
ReentrantLock
的lock
/unlock
2)底层实现原理:
可见性指一个线程修改的变量对其他线程即时可见,其实现依赖缓存刷新与失效机制:
- volatile 的可见性原理:
- 写操作:触发
StoreStore
屏障(强制缓存数据刷新到主内存)+StoreLoad
屏障(确保刷新完成),通过 MESI 协议的 “修改 - 失效” 机制,使其他线程的缓存副本失效; - 读操作:触发
LoadLoad
屏障(清空本地缓存失效队列)+LoadStore
屏障(强制从主内存读取最新值)。
- 写操作:触发
- synchronized 的可见性原理:
解锁(unlock
)时,JVM 会插入StoreStore
屏障,强制将工作内存中的变量刷新到主内存;加锁(lock
)时,插入LoadLoad
屏障,强制从主内存加载最新值,确保线程进入同步块时能看到其他线程的修改。 - final 的可见性原理:
final
变量初始化后不可修改,JVM 在构造函数结束时插入StoreStore
屏障,确保final
变量的值在对象引用可见前已完全初始化,其他线程不会看到 “半初始化” 的final
变量。
.
1.6.3 有序性(Ordering):从指令重排序到内存屏障
Java 程序中的代码 不一定会按照编写的顺序执行。编译器或 CPU 为了优化性能可能会做 指令重排序。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。多线程下,为了避免破坏语义,需要通过happens-before
规则来确保“看起来是有序的”。
1)底层实现原理:
有序性指程序执行顺序符合预期,核心是解决编译器 / CPU 指令重排序导致的多线程语义混乱:
- 重排序的根源:
- 编译器重排序:Java 编译器(如
javac
)为优化性能调整指令顺序(如将无关的读操作提前); - CPU 重排序:处理器通过乱序执行(Out-of-Order Execution) 提升效率(如先执行无依赖的指令)。
- 编译器重排序:Java 编译器(如
- 有序性保证机制:
- happens-before 规则:通过定义操作间的偏序关系,确保多线程下的 “逻辑有序性”(即使物理上重排序,也需满足规则);
- 内存屏障:JVM 通过插入特定屏障指令阻止重排序,映射到硬件指令:
- x86 架构:通过
mfence
(StoreLoad)、lfence
(LoadLoad)、sfence
(StoreStore)实现; - ARM 架构:通过
dmb
(数据内存屏障)、dsb
(数据同步屏障)实现。
- x86 架构:通过
.
二、Java内存模型底层实现 - 案例分析
Java 内存模型(JMM)在 JVM 层的实现涉及字节码生成、内存屏障插入、对象头管理 和 垃圾回收 等多个层面。
在 Java 并发编程中,volatile
、synchronized
、final
等关键字以及显式锁(如ReentrantLock
)的内存语义,均通过 JVM 插入的内存屏障(Memory Barrier)来实现 Happens-Before 原则。下面我们对其核心实现机制进行详细解析
2.1 volatile 关键字的实现
- 字节码层面:对 volatile 变量的读写会生成特殊的字节码指令(如
getfield
/putfield
),并在运行时通过内存屏障保证可见性。 - HotSpot JVM 实现:在 volatile 写操作后插入 StoreLoad 屏障,在读操作前插入 LoadLoad 屏障。例如:
// Happens-Before 关系:1 → 2 → 3 → 4(通过 volatile 的内存屏障和传递性规则)
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1. 普通写
flag = true; // 2. volatile写
// 对应字节码:putfield,JVM会在此插入 StoreStore屏障 → StoreLoad屏障
}
public void reader() {
if (flag) { // 3. volatile读
// 对应字节码:getfield,JVM会在此插入 LoadLoad屏障 → LoadStore屏障
int i = a; // 4. 普通读(一定看到a=1)
}
}
}
.
2.2 synchronized 关键字的实现
- 字节码层面:synchronized 同步代码块通过
monitorenter
和monitorexit
指令实现。synchronized 同步方法通过方法表中的ACC_SYNCHRONIZED
标志位标识。- 锁获取(
monitorenter
):在获取锁后插入LoadLoad 屏障和LoadStore 屏障:确保后续操作读取到最新值 - 锁释放(
monitorexit
):在释放锁前插入StoreStore 屏障和StoreLoad 屏障:确保锁内的所有操作结果刷新到主内存
- 锁获取(
- HotSpot JVM 实现:
- 偏向锁:对象头(
Mark Word
)记录第一个获取锁的线程 ID,无竞争时无需CAS
操作。 - 轻量级锁:通过
CAS
将Mark Word
替换为锁记录指针,失败则膨胀为重量级锁。 - 重量级锁:依赖操作系统的互斥量(
Mutex
),线程挂起和唤醒开销大。
- 偏向锁:对象头(
// Happens-Before 关系:1(锁释放前) → 2(锁获取后)(通过管程锁定规则)
public class SyncExample {
int a = 0;
public synchronized void writer() { // 锁获取
a = 1; // 1. 写操作
} // 锁释放(隐式插入:StoreStore屏障 → StoreLoad屏障)
public synchronized void reader() { // 锁获取(隐式插入:LoadLoad屏障 → LoadStore屏障)
int i = a; // 2. 读操作(一定看到a=1)
}
}
.
2.3 final 关键字的内存屏障
对象构造函数中的final
字段赋值:在构造函数结束前插入StoreStore 屏障:确保 final
字段的写操作先于对象引用对其他线程可见
// Happens-Before 关系:1 → 对象引用对其他线程可见(通过 final 字段的内存语义)
public class FinalExample {
final int x;
int y;
public FinalExample() {
x = 3; // 1. final字段赋值
y = 4; // 2. 普通字段赋值
// 隐式插入:StoreStore屏障(确保x的写操作完成)
}
}
.
2.4 Java 原子类底层实现
Java 原子类(如AtomicInteger
、AtomicReference
等)是java.util.concurrent.atomic
包下的核心组件,其设计直接遵循 JMM(Java 内存模型)的规范,通过硬件原语、volatile
变量和内存屏障的协同,保证多线程环境下的原子性、可见性和有序性。以下从 JMM 三大特性的角度,深度解析原子类的底层实现机制:
2.4.1 原子性保证:基于硬件 CAS 指令与 Unsafe 类
JMM 要求原子操作 “不可分割”,原子类的原子性实现依赖CAS
(Compare-And-Swap
)操作,而 CAS 的底层是硬件级别的原子指令,直接映射到 JMM 对 “原子性” 的定义。
1)CAS 操作的硬件基础
CAS 是一种无锁同步机制,其核心逻辑为:
// CAS伪代码:比较并交换
boolean compareAndSwap(int[] arr, int index, int expect, int update) {
if (arr[index] == expect) { // 比较当前值与预期值
arr[index] = update; // 若相等则更新
return true;
}
return false;
}
该操作通过硬件指令实现原子性:
- x86 架构:映射为
cmpxchg
指令(带lock
前缀时,通过总线锁或缓存锁保证原子性); - ARM 架构:映射为
ldaxr
(加载并标记)和stlxr
(存储并检查标记)指令对,通过LL
/SC
(Load-Linked
/Store-Conditional
)机制保证原子性。
硬件指令的原子性直接满足 JMM 对 “单个操作不可分割” 的要求,是原子类实现原子性的基石。
2) 原子类对 CAS 的封装:Unsafe 类的作用
Java 原子类通过sun.misc.Unsafe
类(底层 JVM 接口)调用 CAS
指令,例如AtomicInteger
的核心实现:
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value; // volatile修饰的共享变量
// 原子更新操作:调用Unsafe的CAS方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 自增操作:循环CAS实现
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
unsafe.compareAndSwapInt
是 native 方法,直接调用底层cmpxchg
(x86)或ldaxr
/stlxr
(ARM)指令,保证操作原子性;- 对于复合操作(如
incrementAndGet
),通过循环 CAS(自旋重试)实现:若一次 CAS 失败(被其他线程修改),则重新读取最新值并再次尝试,直至成功,从而将 “读 - 改 - 写” 复合操作转化为一系列原子 CAS 操作,符合 JMM 对原子性的扩展要求。
.
2.4.2 可见性保证:volatile 变量与缓存刷新
JMM 要求 “一个线程的修改对其他线程可见”,原子类通过volatile
修饰共享变量+CAS
操作的缓存刷新机制满足这一要求。
1)共享变量的 volatile 修饰
所有原子类的核心变量均用volatile修饰,例如:
AtomicInteger
的value
:private volatile int value;AtomicReference
的value
:private volatile V value;
volatile的可见性原理(符合 JMM 规范):
- 写操作:通过StoreStore屏障强制将修改刷新到主内存,并通过 MESI 协议使其他线程的缓存副本失效;
- 读操作:通过LoadLoad屏障强制从主内存读取最新值,避免使用本地缓存的旧值。
这确保原子类的变量修改能被其他线程即时感知,符合 JMM 的可见性标准。
.
2)CAS 操作的缓存同步机制
CAS 操作不仅是原子的,还会触发缓存刷新:
- 当 CAS 成功修改变量时,硬件会自动执行 “缓存写回(Write-Back)” 操作,将修改同步到主内存;
- 其他线程的 CAS 操作会读取主内存的最新值(因旧缓存副本已被 MESI 协议标记为无效),从而保证可见性。
例如,线程 A 通过 CAS 修改AtomicInteger
的value
后,线程 B 的下一次 CAS 会读取主内存的新值,而非本地缓存的旧值,这与 JMM “工作内存→主内存→其他工作内存” 的同步流程完全一致
.
2.4.3 有序性保证:内存屏障与 CAS 的天然有序性
JMM 通过 happens-before 规则和内存屏障保证有序性,原子类通过 CAS
操作的天然有序性 + volatile
的内存屏障满足这一要求。
.
1)CAS 操作的天然有序性
硬件级 CAS 指令具有全序性(Total Order):在多核 CPU 中,所有 CAS 操作的执行顺序在全局可见,且任一 CAS 操作的结果对后续操作可见。这种硬件特性天然符合 JMM 的 “有序性” 要求 —— 即使编译器或 CPU 进行指令重排序,CAS 操作的相对顺序也不会被打破。
例如,线程 A 的CAS(value, 1, 2)
操作必然 happens-before 线程 B 的 CAS(value, 2, 3)
操作,因为 B 的 CAS 依赖 A 的结果,硬件会保证这种顺序。
.
2)volatile 的内存屏障增强有序性
原子类中 volatile
变量的读写会插入内存屏障,进一步阻止指令重排序:
- 写操作(如
set
方法):JVM 会在volatile
写后插入StoreLoad
屏障,确保写操作完成后再执行后续读操作; - 读操作(如
get
方法):JVM 会在volatile
读前插入LoadLoad
屏障,确保读操作前的所有读操作已完成。
以AtomicInteger
的set方法为例:
public final void set(int newValue) {
value = newValue; // volatile写,触发StoreStore+StoreLoad屏障
}
这保证了set操作的结果对后续所有操作可见,符合 JMM 的volatile
变量规则(写操作 happens-before
后续读操作)。
.
2.4.4 原子类与 JMM 内存交互操作的对应
JMM 定义的 8 种内存交互操作(lock
/unlock
/read
/load
/use
/assign
/store
/write
),在原子类中均有明确映射:
例如,AtomicInteger
.incrementAndGet()
的流程完全遵循 JMM 交互规则:
read
:从主内存读取value
到工作内存;load
:载入寄存器,计算value+1
;use
:将value
作为预期值,value+1
作为更新值,执行 CAS;- 若 CAS 成功:
assign
(工作内存变量设为value+1
)→store
→write
(同步到主内存); - 若 CAS 失败:重复 1-4,直至成功。
.
2.4.5 特殊原子类的扩展实现(以 LongAdder 为例)
对于高并发场景,LongAdder
等原子类通过 “分段锁” 思想优化性能,但仍严格遵循 JMM:
- 分段设计:将全局变量拆分为多个
Cell
(@Contended
修饰,避免伪共享),每个Cell
是volatile long
; - 原子性:对单个
Cell
的修改通过 CAS 实现,全局结果通过累加所有Cell
的值获得; - 可见性:
Cell
的volatile
修饰保证单个分段的修改可见; - 有序性:
Cell
的读写插入内存屏障,符合happens-before
规则。
这种设计在提升性能的同时,未破坏 JMM 的任何规范,是 “高效性” 与 “规范性” 平衡的典范。
.
2.5 显式锁(如ReentrantLock)的内存屏障
ReentrantLock
基于 AQS(AbstractQueuedSynchronizer)
实现,其内存屏障与synchronized
类似:
lock ()
方法:在获取锁后插入内存屏障,确保后续操作读取到最新值。unlock ()
方法:在释放锁前插入内存屏障,确保锁内操作结果刷新到主内存。
// Happens-Before 关系:与synchronized一致,通过锁的获取和释放插入内存屏障
public class LockExample {
private final Lock lock = new ReentrantLock();
int a = 0;
public void writer() {
lock.lock();
try {
a = 1; // 1. 写操作
} finally {
lock.unlock(); // 隐式插入:StoreStore屏障 → StoreLoad屏障
}
}
public void reader() {
lock.lock();
try {
int i = a; // 2. 读操作(一定看到a=1)
} finally {
lock.unlock();
}
}
}
.
.
三、JMM 的实践陷阱与优化策略
理解 JMM 的底层机制是避免并发陷阱的关键,常见问题与解决方案:
3.1 伪共享(False Sharing)
问题:多个线程修改同一缓存行中的不同变量,导致缓存行频繁失效(MESI 协议触发),性能下降 70% 以上。
// 两个变量共享同一缓存行(64字节)
int a = 0;
int b = 0;
// 线程1频繁修改a,线程2频繁修改b,导致缓存行反复失效
解决方案:
- 使用
@Contended
注解(JDK8+):自动填充缓存行,确保变量独占 64 字节; - 手动填充:添加冗余字段至 64 字节(如 7 个 long 类型字段)。
.
3.2 volatile 的滥用
问题:volatile
仅保证可见性和有序性,不保证原子性,滥用会导致错误。
volatile int count = 0;
// 多线程执行count++,结果可能小于预期(非原子操作)
解决方案:
- 原子类:
AtomicInteger
(CAS 保证原子性); - 结合锁:
synchronized
或Lock
确保复合操作原子性。
.
3.3 指令重排序的隐蔽问题
问题:编译器重排序可能破坏多线程语义,即使无数据依赖。
int a = 0, b = 0;
boolean flag = false;
// 线程A
a = 1; // 操作1
flag = true; // 操作2
// 线程B
if (flag) { // 操作3
b = a; // 操作4,可能b=0(操作2被重排到操作1前)
}
解决方案:
- 添加
volatile
:volatile boolean flag,通过 StoreLoad 屏障阻止重排序; - 使用
synchronized
:通过锁的有序性保证操作顺序。
.
.
四、JMM 的设计本质与价值
Java 内存模型是 “硬件复杂性
” 与 “软件易用性
” 的折中:
- 底层依赖 CPU 缓存、总线锁、MESI 协议等硬件机制;
- 上层通过
happens-before
规则、内存屏障等软件规范,为开发者提供统一的并发抽象,屏蔽硬件差异。
掌握 JMM 不仅能避免并发陷阱,更能指导高性能并发程序的设计(如减少 volatile 使用、优化缓存行对齐、合理选择同步机制)。未来 JMM 将持续演进,以适应异构计算(GPU、NVM)等新场景,但其 “安全性与性能平衡” 的核心设计哲学将始终不变。