内存可见性 / happens-before / 为什么 volatile / 为什么锁一定要配内存屏障
2026/5/8 10:15:49 网站建设 项目流程

目标只有一句话:解释清楚:为什么“加了锁 / CAS / volatile”,另一个线程才能“看见”你的写入。

四个核心问题:

  1. 为什么多线程下“写了 ≠ 别人能立刻看到”?

  2. 什么是 happens-before?它解决的是什么问题?

  3. volatile 到底保证了什么?不保证什么?

  4. 为什么锁一定要配合内存屏障?否则会发生什么?

为什么“写了”≠“别人能看到”?

这是 今天的起点 的起点。

在 锁、互斥、阻塞、自旋、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:mfence

  • ARM:dmb ish

  • RISC-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 的代价”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询