Java 并发核心机制总结:原子性、CAS、内存屏障、缓存一致性协议(MESI)¶
本笔记围绕以下几个关键词展开:原子性(Atomicity)、CAS(Compare-And-Swap)、内存屏障(Memory Barrier)、缓存一致性协议(MESI)。通过逐步深入的逻辑与具体示例,我们将从根源解释这些机制是如何协同工作以确保并发安全的。
一、原子性是什么?¶
原子性指操作是不可中断的最小执行单元,要么全部执行完成,要么完全不执行。在并发环境中,原子性可防止“线程间中间状态可见”。
示例:非原子操作的危险¶
i++; // 实际上包括:读取 i → 自增 → 写回 i
在并发中,多个线程可能读取同一值,导致结果不一致。
二、CAS:实现原子性的硬件机制¶
CAS(Compare-And-Swap)是一种乐观锁机制,基于硬件指令实现原子更新。
if (memory == expected) {
memory = newValue;
return true;
} else {
return false;
}
底层实现¶
- x86 架构:使用
lock cmpxchg
实现。 lock
前缀在多核中用于通知其他核心暂停对该缓存行的访问(通过总线锁或缓存一致性协议)。
三、现代 CPU 是如何实现 CAS 原子性的?¶
- 锁住总线(旧方法,昂贵,影响性能)
- 缓存一致性协议(MESI):现代 CPU 优选方案,精细控制缓存同步。
MESI 协议四个状态¶
状态 | 含义 |
---|---|
M(Modified) | 当前核心独占且已修改 |
E(Exclusive) | 当前核心独占但未修改 |
S(Shared) | 多核心共享,主存一致 |
I(Invalid) | 无效缓存,需重新拉取 |
MESI 如何保障 CAS 原子性?¶
- CAS 操作前获取该地址缓存行的 Exclusive 或 Modified 权限。
- 其他核心如需访问该地址,需先通过协议协商,不能直接从 L3 或主存读取。
四、CAS 本身是否足够?¶
虽然 CAS 能保证原子性,但无法防止前后的指令被 CPU 或编译器重排,导致并发错误。
示例:发布-消费模型中的重排序问题¶
obj.value = 42;
cas(obj.flag, 0, 1); // 发布完成信号
如果发生重排序:
cas(obj.flag, 0, 1);
obj.value = 42;
另一个线程看到 flag == 1 后读取 value,可能读到旧值。
五、内存屏障的作用:保证可见性与有序性¶
内存屏障(Memory Barrier)用于防止重排序、控制可见性。
屏障类型 | 含义 |
---|---|
LoadLoad | 防止前后读乱序 |
StoreStore | 防止前后写乱序 |
LoadStore | 防止读后写提前 |
StoreLoad | 强屏障,常用于 volatile |
CAS 搭配的内存屏障¶
- CAS 前:插入 StoreStore 屏障,确保之前写的内容(如 obj.value)不会被重排到 CAS 之后。
- CAS 后:插入 LoadLoad + LoadStore 屏障,保证读写操作不会被提前到 CAS 之前。
六、总结:并发安全三要素协同工作¶
特性 | 实现机制 | 示例 |
---|---|---|
原子性 | CAS(lock cmpxchg) | AtomicInteger.incrementAndGet() |
可见性 | 内存屏障 / volatile | volatile 保证刷新主内存 |
有序性 | 内存屏障防止指令重排 | obj.value -> CAS(obj.flag) 顺序保证 |
七、关键反驳与实验证明¶
有人认为:
“即使顺序是 CAS -> value 写也没事,线程看到 flag==1 后 value 应该也有了。”
我们用“发布-消费”模型反驳该观点:
Thread A:
obj.value = 42;
cas(obj.flag, 0, 1); // 正确顺序,value 先初始化
Thread B:
if (obj.flag == 1) {
read(obj.value); // 安全
}
如果重排序成:
Thread A:
cas(obj.flag, 0, 1);
obj.value = 42;
Thread B:
if (obj.flag == 1) {
read(obj.value); // 可能读到旧值,错误!
}
结论:原子性 ≠ 并发安全。完整并发语义必须靠内存屏障保障顺序性与可见性。
八、延伸思考¶
- MESI 之外还有 MOESI、MESIF 等协议,进一步优化缓存共享。
- Java 9 之后的 VarHandle 提供更底层可控的内存语义。
- 高性能队列(如 Disruptor)广泛应用 CAS + 内存屏障技术。
九、深入扩展:Redis、Lock 前缀、总线锁与缓存一致性协议¶
本节我们从 Redis 的单线程模型引出硬件级并发控制机制,深入理解 lock
指令、总线锁、MESI 缓存一致性协议三者的关系与区别。
1. Redis 的并发模型与原子性¶
- Redis 是单线程处理命令(事件循环 + IO 多路复用)
- 命令如
DECR key
是逻辑原子的,但不是 CPU 层面的单指令 - 实际操作:
LOAD → DEC → STORE
,但 Redis 串行执行,天然顺序和线程安全
2. DECR
在硬件上的类比¶
在 CPU 层,相当于:
LOAD R1, [key] ; 加载
DEC R1 ; 自减
STORE [key], R1 ; 写回
- 并非原子,需要额外机制来保证“整体不可中断”
3. lock
前缀:请求原子性的标志¶
lock cmpxchg [mem], eax
lock
是 x86 架构中告诉 CPU:我需要对这条内存操作执行原子性保障- 不能被中断、不能和其他核心交错执行
4. 总线锁(Bus Lock):早期机制¶
- 早期 CPU 使用 锁定地址总线 的方式来阻止其他核心访问共享内存
- 优点:实现简单
- 缺点:
- 粒度粗,整个系统总线暂停
- 影响大,不可扩展
5. 缓存一致性协议(MESI):现代机制¶
- 当
lock
操作针对的是可缓存地址时(RAM),现代 CPU 使用 MESI 协议 - 将目标地址所在缓存行标记为
M
(Modified) - 通知其他核心将该行置为
I
(Invalid) - 达到锁定缓存行、保持原子性的效果
6. 总线锁 vs 缓存一致性协议:区别对比¶
项目 | 总线锁(Bus Lock) | MESI 缓存一致性协议 |
---|---|---|
锁定范围 | 所有内存访问总线 | 仅相关缓存行 |
粒度 | 粗,系统级 | 精细,按缓存行 |
并发效率 | 低 | 高 |
是否阻塞无关操作 | 是 | 否 |
现代架构使用 | 少 | 主流 |
7. MESI 协议 + lock 的执行流程¶
Core A Core B
------ ------
L1: [X:M] L1: [X:I] ← B 无效
执行 lock cmpxchg
→ A 修改缓存行
→ 操作结束前,其他核心不能访问 X
→ 完成后同步可见性
- 利用缓存协议保持“看似一瞬间完成”的原子性
- 无需锁总线
8. 特殊情况:仍然使用总线锁的场景¶
场景 | 是否使用 Bus Lock |
---|---|
操作 MMIO(设备内存) | 是 |
不可缓存内存 | 是 |
普通变量 / 数组元素 | 否,使用 MESI |
9. 总结¶
lock
是语义标志,具体如何实现原子性取决于底层硬件:
- 对普通内存:使用 MESI 协议锁缓存行
- 对 IO 地址:使用总线锁
Redis 不使用 lock 或硬件原子性,而是靠事件循环串行执行实现“逻辑原子性”。