目标只有一句话:解释清楚:为什么“加了锁 / CAS / volatile”,另一个线程才能“看见”你的写入。
四个核心问题:
为什么多线程下“写了 ≠ 别人能立刻看到”?
什么是 happens-before?它解决的是什么问题?
volatile 到底保证了什么?不保证什么?
为什么锁一定要配合内存屏障?否则会发生什么?
为什么“写了”≠“别人能看到”?
这是 今天的起点 的起点。
在 锁、互斥、阻塞、自旋、CAS、可见性 篇已经知道:
锁能保证“同一时刻只有一个线程进入临界区”
CAS 能保证“某一步操作是原子的”
但你还没有保证一件事:
写入什么时候对其他线程可见?
直觉模型:
Thread A: x = 1
Thread B: if (x == 1) ...
“既然 A 写了,B 就一定能看到”。
这是错的。
在现代系统中:
CPU 有寄存器
CPU 有多级缓存
编译器会重排序指令
CPU 会乱序执行
结果是:
Thread A 的写入,可能只存在于 A 的寄存器 / cache / store buffer 中,
Thread B 完全看不到。
这不是 Bug,是为了性能而必须存在的行为。
happens-before:并发世界里的“因果关系”
因为“时间顺序”在多线程下不可靠,
所以 OS / JVM / 内存模型引入了一个更强的概念:
happens-before
定义:
如果 A happens-before B,那么 A 的所有写入,在 B 执行时一定是可见的。
不是“先执行”,而是“先可见”。
happens-before 不是自然存在的
下面这些默认都不成立:
线程启动顺序
代码书写顺序
CPU 执行顺序
时间戳
happens-before只能通过规则建立。
三条必须记住的 happens-before 规则(最小集)
① 程序顺序规则(单线程内)
A; B;
A happens-before B(仅限同一线程)。
② 锁规则(最重要)
对同一把锁的 unlock happens-before 后续的 lock。
这句话非常关键:
A 解锁前的所有写入
对 B 来说,在加锁后一定可见
③ volatile 规则
对 volatile 变量的写 happens-before 后续对它的读。
这是 volatile 存在的全部意义。
volatile:它到底保证了什么?
这是最容易被神话、也最容易被误用的关键字。
volatile 保证的只有两件事
① 可见性
一个线程写 volatile 变量,
其他线程读它,一定能看到最新值。
② 禁止部分重排序
volatile 写之前的普通写
不会被重排到 volatile 写之后volatile 读之后的普通读
不会被重排到 volatile 读之前
这是为了支撑 happens-before。
volatile不保证的事(非常重要)
不保证原子性
volatile int x;
x++
依然是竞态。
不保证互斥
多个线程可以同时读写 volatile。
一句话总结 volatile
volatile 只解决“看不看得见”,
不解决“能不能一起改”。
为什么锁一定要配合内存屏障?
在 锁、互斥、阻塞、自旋、CAS、可见性 学到:
锁保证同一时刻只有一个线程进入临界区。
但如果锁只限制进入,不限制内存行为,会发生什么?
如果锁没有内存语义
假设:
Thread A: lock() x = 1 unlock() Thread B: lock() if (x == 1) ... unlock()
如果:
x=1 被缓存
unlock 没有 flush
lock 没有 refresh
那么:
Thread B 进入了临界区,却依然读到 x=0
这在没有内存屏障的机器上是完全可能的。
正确的锁语义(所有现代 OS / JVM 都保证)
锁的 unlock:Release 屏障
把当前线程的写入刷新到共享内存
锁的 lock:Acquire 屏障
使后续读一定能看到之前释放的写入
因果关系
unlock happens-before 后续对同一把锁的 lock。
这就是锁既解决“互斥”,又解决“可见性”的原因。
一句话总纲
并发错误的根因不是“线程多”,
而是“没有建立 happens-before 关系”。
训练题:
Q1:为什么线程 A 写了变量,线程 B 可能看不到?
因为编译器重排序、CPU 缓存和乱序执行,写入可能未对其他线程可见。
Q2:happens-before 的核心含义是什么?
如果 A happens-before B,则 A 的所有写入在 B 执行时必然可见。
Q3:volatile 保证了什么?不保证什么?
volatile 保证可见性和禁止部分重排序,但不保证原子性和互斥。
Q4:为什么“unlock happens-before lock”这么关键?
因为 unlock 前的写入在后续 lock 后必然对其他线程可见,从而建立可见性和有序性。
Q5:为什么只用 CAS 还不够,仍然需要内存语义?
因为 CAS 只保证单次原子更新,不自动建立完整的内存可见性与 happens-before 关系。
最终收束:
① 多线程下写入不一定可见,因为编译器与 CPU 会进行重排序和缓存优化。
② happens-before 定义了并发中的因果关系:先发生的写入必须对后续执行可见。
③ volatile 用于建立可见性与有序性,但不提供原子性和互斥。
④ 锁通过 unlock→lock 建立 happens-before,同时解决互斥与可见性问题。
⑤ CAS 只能保证原子更新,复杂并发仍需要明确的内存语义保障。
原思路:
1,因为可能还在某个核心的sb里,或者b读到了旧的缓存
直接跳到了CPU 实现细节,而 Day6 要的是抽象层结论
在 OS / 内存模型层,正确原因是:
最小正确答案:
因为编译器重排序、CPU 缓存和乱序执行,写入可能未对其他线程可见。
说的 SB / cache 是why,这句是what。
2,保证所有写入对于其他执行一定可见
接近,但不够精确,有歧义
“其他执行”太泛,容易被理解成“全局可见”。最小正确答案:
如果 A happens-before B,则 A 的所有写入在 B 执行时必然可见。
这是严格定义,不能缩。
3,保证可见性和部分执行顺序不重排,但是不保证原子性和互斥
4,因为unlock 前的写入在后续 lock 后对其他线程必然可见
5,只用CAS只能保证单个变量的原子性操作,无法保护复杂临界区
✔方向对,但这是 Day4 的答案,不是这篇的
关键不是“临界区大小”,
而是内存语义。最小正确答案:
因为 CAS 只保证原子更新,不自动建立完整的 happens-before 关系和内存可见性。
疑惑:现代cpu几乎没有不存在storeload屏障的吧?那q1不就是storeload屏障解决的问题吗?
不对,Q1 不是被“CPU 默认就有 StoreLoad 屏障”解决的问题。
恰恰相反:Q1 之所以存在,是因为 CPU 默认没有全局 StoreLoad 屏障。
现代 CPU当然“支持” StoreLoad 屏障指令,
但默认执行路径里并不存在“隐式的全局 StoreLoad 屏障”。
| 混在一起的两件事 | 实际情况 |
|---|---|
| CPU支持屏障 | ✅ 是的 |
| CPU默认执行屏障 | ❌ 完全不是 |
区分:“存在” vs “自动发生”
现代 CPU 的事实是:
有StoreLoad / StoreStore / LoadLoad / LoadStore这些屏障能力
也有fence / mfence / dmb / sync等指令
但默认执行路径是:
没有任何全局内存屏障。
否则会发生什么?
CPU 会慢到不可接受。
为什么 CPU 不可能“默认就有 StoreLoad 屏障”?
事实1:StoreLoad 是最“重”的屏障
StoreLoad 屏障意味着什么?
在这条指令之前的所有写入,
在这条指令之后的所有读取之前,
必须对所有核心可见。
这会强制:
清空 Store Buffer
同步 cache coherence
阻止乱序执行
阻止编译器重排
如果 CPU每次写完都隐式插一个 StoreLoad:
多核扩展性直接崩
pipeline 深度失效
乱序执行收益消失
现代 CPU 绝对不可能这么做。
事实2:CPU 的默认策略:尽量不保证 Store→Load 顺序
这就是为什么:
Store Buffer 存在
Load 可以越过 Store
写入可以“暂存”在本核
这些设计不是 bug,是性能核心来源。
那 StoreLoad 屏障到底什么时候出现?
只有在三种情况下,CPU 才会真的执行 StoreLoad 屏障语义:
① 显式内存屏障指令
比如:
x86:
mfenceARM:
dmb ishRISC-V:
fence rw, rw
这是程序员 / 编译器主动要求的。
② 同步原语的实现中(锁 / CAS / 原子操作)
这是最重要的一点。
例如:
mutex.unlock()→release 屏障mutex.lock()→acquire 屏障atomic.compare_exchange→带内存序语义
也就是说:
你只有在用“同步原语”时,CPU 才会执行必要的屏障。
③ 架构“偶然”比规范更强(如 x86 TSO)
x86 的确比 ARM 强:
StoreStore、LoadLoad 多数是天然保证的
但StoreLoad 仍然不保证
所以即使在 x86:
A 写了,B 依然可能读不到(跨核)
只是概率和窗口更小。
现在重新回答 Q1,但用“屏障视角”:
因为在 A 的写入和 B 的读取之间,没有任何同步原语插入 StoreLoad / Release-Acquire 屏障。
换句话说:
不是“缺少屏障能力”,而是“你没有要求 CPU 执行屏障”。
Q1 的根因不是“CPU 不懂可见性”,而是“CPU 默认不会为你建立 happens-before,除非你显式使用带内存语义的同步原语”。
最后总结:
现代 CPU 并不是“默认就有 StoreLoad 屏障”,
而是“只在你通过同步原语请求时,才付出 StoreLoad 的代价”。