news 2026/5/15 1:58:18

内存可见性 / happens-before / 为什么 volatile / 为什么锁一定要配内存屏障

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
内存可见性 / happens-before / 为什么 volatile / 为什么锁一定要配内存屏障

目标只有一句话:解释清楚:为什么“加了锁 / 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 的代价”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 7:01:47

Python与自动化测试:提高软件质量和稳定性

在软件开发过程中,自动化测试是提高软件质量和稳定性的重要手段之一。Python作为一种简洁而强大的编程语言,为自动化测试提供了丰富的工具和库。本文将介绍几个常见的自动化测试案例,并提供详细的Python代码示例,帮助您更好地理解…

作者头像 李华
网站建设 2026/5/8 3:40:44

3年测试经验,测试用例应该达到这个水平才合格

状态迁移法主要关注在测试状态转移的正确性上面。对于一个有限状态机,通过测试验证其在给定的条件内是否能够产生需要的状态变化,有没有不可达的状态和非法的状态,是否可能产生非法的状态转移等。通过构造能导致状态迁移的事件,来…

作者头像 李华
网站建设 2026/5/8 4:23:57

逻辑回归调优三板斧:参数调整、阈值设定、数据集平衡

在上一篇博客中我们建立的逻辑回归模型解决了银行贷款的二分类问题,但是不知道大家有没有注意到我们当时的召回率是非常低的,只有百分之百,这一次,我们就来学习如何进行逻辑回归的调优。、 1、参数调整 原理 在逻辑回归的参数调…

作者头像 李华
网站建设 2026/5/13 18:15:22

AI 写论文哪个软件最好?别再被 “秒出万字” 忽悠!真正能过导师审的,只有虎贲等考 AI—— 敢把数据和文献摊开给你验!

🤯 你是不是也被 “秒出万字”“一键生成论文” 的 AI 工具宣传冲昏过头脑?我曾经跟风用某热门工具写课程论文,20 分钟拿到万字初稿,本以为捡了大便宜,结果被导师一顿痛批:“参考文献一半查无原文&#xff…

作者头像 李华
网站建设 2026/5/10 1:50:37

控制电缆一键测量-批量测量告别繁琐手算

控制电缆一键测量-批量测量告别繁琐手算 控制电缆工程量依据设计图示尺寸,按单根电缆的总长度加预留长度计算。借助CAD快速看图软件的【批量测量】功能,可快速按图层选取测量对象,自动累积长度,一键生成统计结果,实现…

作者头像 李华
网站建设 2026/5/14 7:51:25

国产 Linux 系统核心优缺点与适用场景

发行版核心优点主要缺点适用人群 / 场景银河麒麟(KylinOS)1. 等保四级认证,内核级安全(MAC/TCM),适配党政军高安全需求2. 全面兼容飞腾、龙芯、鲲鹏等国产 CPU,性能损耗低3. 与达梦、WPS 等 200…

作者头像 李华