想象一家名叫“并发厨房”的深夜小馆:主厨、二厨、甜品师各占一个灶台,同时开火(这就是多线程)。后厨正中央有一张主操作台,摆着全店共享的菜谱卡和食材(主内存);而每位厨师手边都有一个小料台,放着常用调料的私人备份(工作内存 / CPU 缓存)。麻烦来了——主厨刚把改良的秘制辣酱配方放上主操作台,甜品师的小料台上却还摆着旧配方,于是他一勺下去,整块提拉米苏都串了味。更糟的是,主厨觉得“先切葱再热油”更快、悄悄调整了做菜步骤(重排序),单独做菜没问题,可一旦要和别人配合,整个厨房的秩序就崩了。 内存模型,就是这家厨房里那本看不见的《后厨行为准则》——它规定每个线程能对另一个线程的行动做多大程度的假设。而 Happens-Before(先行发生)是这本准则的核心条款:只要一个动作“先行发生”于另一个动作,前者的成果就保证被后者看见。没有它,厨房就是纯粹的混沌——程序能跑,但偶尔会端出一盘“盐当糖”的黑暗料理。这篇文章,就带你读懂这份隐形秩序,从主内存与工作内存的模型,到三大并发特性、指令重排序,再到整套 Happens-Before 规则,以及锁、volatile 和实战排查。
一句话价值:大模型推理服务、特征缓存更新、分布式训练同步,本质都是“多线程共享内存”问题。吃透内存模型与 Happens-Before,你写的服务才能在流量洪峰下不“串味”,这也是高并发系统设计面试的必考基石。
目录
可视化演示
演示内存模型下 happen-before(先行发生)原则如何界定多线程事件之间的可见顺序,步进切换查看程序顺序、锁规则、volatile 规则与传递性,理解并发编程的可见性保障。
本节配有交互动效(进入视口后自动加载,约 3KB)。
happen-before 先行发生 程序顺序保证单线程内有序 锁规则保证解锁先于加锁 volatile 保证写先于读 可见 = 有序 传递性串联多线程关系
一、内存模型图解——主操作台与小料台
在“并发厨房”里,最贵重的是正中央那张主操作台,上面摆着全店共享的菜谱卡与食材——这就是主内存。但每位厨师手边都有一方小料台,放着常用调料的私人备份——这就是工作内存,也叫 CPU 缓存。做菜时,厨师总是先从小料台取用,而不是每次都百米冲刺跑去主操作台。这个设计让单人做菜飞快,却埋下了隐患。
隐患在于:小料台上的“备份”并不总是最新鲜的。主厨刚把新配方放上主操作台(写主内存),甜品师的小料台却还摆着五年前的旧配方(旧缓存),于是甜品师做出的味道,永远慢了主厨半拍。这正是内存模型要解决的核心问题:跨线程共享数据时,读到的到底是不是最新值。
以 Java 内存模型(JMM)为代表,所有共享变量都存于主内存,每个线程拥有自己的工作内存。线程只能直接操作自己的工作内存,不能直接碰主内存。两个线程想通信,必须绕行“工作内存 → 主内存 → 对方工作内存”这条迂回路。这条路上的任何一环滞后,都会出现“脏读”。
每个厨师都以为“世界以我为中心”,但真正的菜谱卡永远挂在那张主操作台上。要让所有厨师用同一版本干活,就必须立规矩——这正是后面的 Happens-Before 要充当的“厨房宪章”。
- 共享变量归属主内存只要被多个线程访问的变量 x,其权威版本存放在所有线程共用的主内存中,如同菜谱卡挂在主操作台上。
- 线程持有私有副本每个线程 Tᵢ 在自家工作内存中维护副本 x′,读写都先发生在副本上,就像厨师先在小料台调料。
- 副本与主存双向同步副本与主内存通过 read/write 动作来回同步;同步时机一旦错位,读到的就是过期值。
二、三大并发特性——完整、可见、有序
做菜有三大基本功:一盘菜必须完整端出,不能端半个盘子;端出的菜必须让客人看见,不能藏起来;上菜顺序不能乱,凉菜不能比汤还晚。并发编程里,这三件事分别叫原子性、可见性、有序性。
原子性指一个操作不可分割:x = x + 1 必须像“盛一勺酱”一样一气呵成,不能盛到一半被别的厨师抢了勺子。可见性指一个线程写共享变量后,其他线程能立刻看到新值——小料台的备份必须跟随主操作台更新。有序性指程序看起来按代码顺序执行,不能让编译器或 CPU 悄悄调戏你的逻辑。
三者经常集体作妖。比如 i++ 看似一行代码,实则是“读-改-写”三步。没有原子性保护时,两个厨师同时执行 i++,各读出 1、各写回 2,本应加两次的计数只加了一次——这是教科书级别的数据竞争。
厨房从不要求厨师时刻完美,而是用工具保障这三大基本功:锁保原子,volatile 保可见,Happens-Before 保有序——三种法器各司其职,缺一不可。
- 原子性
x = x + 1包含读、加、写三步,必须一气呵成完成;否则两个线程各读各的、各写各的,会丢掉一次更新。 - 可见性写线程更新共享变量后,读线程必须能读到新值,不能一直盯着小料台上的过期备份。
- 有序性单线程内允许重排序以提速,但跨线程共享时,有序性需要 Happens-Before 来兜底。
三、指令重排序——偷懒的厨师与底线
厨师们有个偷懒提速的爱好:菜谱明明写“先热油再下葱”,他觉得“先切葱再热油”更顺手的,只要最终菜的味道一模一样,中间怎么折腾没人管。这就是重排序。操作系统里的编译器、处理器、内存系统,个个都是这样的“偷懒厨师”。
重排序有三个“作案者”:编译器会调整语句顺序以优化寄存器使用,处理器会乱序执行以填满流水线,内存系统会合并且批量写入缓存。它们各自为了性能而折腾,单线程内毫无风险,风险全藏在跨线程协作里。
但重排序有条底线:as-if-serial——单线程内随便折腾,最终结果必须与顺序执行完全一致。然而一旦跨线程共享数据且没有同步,底线立即失效:比如 A 线程本应先写数据再写标志位,被重排成先写标志位,B 线程看到标志位就冲去读数据,结果读到个半成品。这就是“标志位陷阱”。
偷懒的厨师在单人灶台上怎么折腾都行,可一旦要给别人的菜当配料,就必须按宪章来——Happens-Before 正是那本宪章。
- 单线程底线只要最终结果与顺序执行一致,编译器与 CPU 就可以任意调整指令顺序,这保证了程序员的直觉在单线程内不会失灵。
- 跨线程失控多线程共享变量且无同步时,重排可以让“先写数据再写标志”变成“先写标志”,读者读到未就绪的数据。
四、Happens-Before 规则总览——厨房宪章
为了对抗混沌,厨房立了宪章——Happens-Before(先行发生)。核心条文只有一句话:若操作 A happens-before 操作 B,则 A 的一切结果对 B 可见,且 A 必须排在 B 前面执行。它就像传菜窗口的交接规矩:出菜人把菜放上窗口,接菜人从窗口取菜,出菜前的一切努力都随菜完整传给了接菜人。
JSR-133 为 Java 定义了整套 Happens-Before 规则,其中最重要的有:程序次序规则(线程内代码顺序)、锁规则(解锁先行于同一把锁的后续加锁)、volatile 变量规则(对易变变量的写先行于后续读)、传递性规则等。这些规则凑在一起,构成了并发秩序的总纲。
传递性是整套规则的黏合剂。单看 A 和 C 没有直接联系,但通过 B 这个“中间传菜人”,A 的成果也能保证被 C 看见。设计无锁或弱同步算法时,传递性是推理的关键工具——比如通过一个 volatile 标志位,把一堆普通变量的写“捎带”给另一个线程。
宪章立在墙上,但厨房里还有几个特殊的“交接仪式”——开新灶、等传菜员回来、给菜封口上桌。这些仪式都有自己的 Happens-Before 条款。
- HB 的定义若 A 先行发生于 B,则 A 的写结果对 B 的读可见,且执行顺序上 A 严格在 B 之前。
- 传递性若 A 到 B、B 到 C 分别有 HB 关系,则 A 到 C 也必然有;中间人 B 起到了接力棒的作用。
五、线程协同规则——开火与收工的交接
厨房里,有些交接不靠传菜窗口,而是靠仪式。新开一个灶台(启动线程),老灶台要先把火候经验传过去;等传菜员回来(等待线程结束),他一路上的见闻都该补全给你;给菜品封口上桌(对象发布),也要保证客人拿到的是一道完整菜而不是半成品。
start 规则:线程 A 调用 B.start() 之前的所有写,对 B 线程内的所有动作可见——新灶台开火前,老灶台的火候经验必须传过去。join 规则:B 线程内的所有写,对 B.join() 返回之后的 A 线程可见——传菜员回来后,他路上发生的一切你都知道,不需要额外加锁。
此外还有 final 规则:只要对象在构造完成后被正确发布,其他线程读取其 final 字段时必见初始化值,无需额外同步。这像“封口菜”——上桌前已经定妆,不允许任何人再动筷子。
这些仪式让“启动、等待、终止”本身变成内存同步的通道。但日常更多时候,我们还需要主动掏出三件法器——广播、钥匙与自助核对。
- start 规则调用线程在 start 之前的所有共享变量写,对新线程内的所有读操作可见,仿佛开火前先交接火候。
- join 规则被 join 线程内最后一条语句的执行,先行发生于 join 方法成功返回之后,调用线程能看见子线程全部写操作。
- final 规则final 字段在构造结束后持锁发布,其他线程无需同步即可读到完整初始化值,实现安全发布。
六、同步实践——广播、钥匙与自助核对
有了宪章,厨师们按规矩掏出三件法器来维护秩序:广播(volatile)、冰箱钥匙(锁)、自助核对(CAS)。
volatile 是轻量级广播:写 volatile 变量时,把写刷入主内存并让其他线程的副本失效;读时强制从主内存取新值。它天生适合“单写多读”的状态标志,比如特征版本号、服务开关。但广播不能互斥——volatile int i++ 依然是“读-加-写”三步,该乱还是乱,复合操作必须交给锁或原子类。
synchronized / ReentrantLock 的 unlock–lock 建立完整 HB 通道:解锁前所有写,对随后拿到同一把锁的线程可见。冰箱钥匙的道理就在这——不只保证“一个时间一个人进冰箱”,还保证后进冰箱的人能看到前人留下的全部痕迹。原子类(CAS) 则用乐观自旋实现无锁更新:读出旧值、比对、写回,失败就重试,用于计数器等高竞争场景。
三件法器齐了,还得学会“安全发布”——只有让对象以正确姿势端上桌,客人才不会吃到盖了半截章的半成品。这正是排查事故时最先要检查的环节。
- 锁规则同一把锁的解锁先行发生于后续加锁,解锁前的全部写操作对加锁后的线程可见。
- volatile 规则volatile 写先行发生于后续同变量的读;写线程刷主存,读线程强制读主存。
- CAS 自旋原子类用 compare-and-swap 循环,把旧值 aᵢ 更新为新值 aᵢ₊₁,失败即重试,无锁实现原子更新。
七、实战排查——事故现场还原
即使立了宪章,厨房偶尔也会出事:两个人同时改同一份菜谱,且彼此之间没有搭上任何交接通道——这就是数据竞争(data race)。它是内存模型领域最经典的“幽灵故障”。
数据竞争的定义很严格:同一变量被至少两个线程访问,其中至少一个是写,且读写之间没有 happens-before 排序。注意:“两个线程同时读”不算数据竞争;无同步的“读写缠绕”才算。它和“竞态条件”不同——竞态条件说的是逻辑顺序问题,数据竞争说的是内存可见性问题。
数据竞争的症状极具欺骗性:偶发错值、莫名死循环、性能抖动,跑一万次测试可能一次都不发作,一上线就崩。排查手法有四步:第一步由代码评审对照 Happens-Before 清单,找出所有共享变量及其访问点;第二步给可疑字段加 volatile 观察症状是否消失;第三步用竞争检测工具(如 Java Flight Recorder、ThreadSanitizer)抓现场;第四步修复后回归验证并用压测复现。
事故还原完毕,教训深刻:改共享状态前先自问三连——谁来写、谁来读、HB 链在哪?把共享边界最小化,能表达为 final/volatile 的绝不上锁,能上锁的绝不依赖巧合时序。
- 有写者存在至少一个线程对变量 x 执行写操作,这是数据竞争的必要前提。
- 无 HB 排序访问 x 的各操作之间不存在任何 Happens-Before 链,编译器与 CPU 因而可以任意重排。
AI 应用场景
AI 推理服务的特征快照热更新
阶段:模型部署与在线推理
解决的问题:推理引擎为降低特征 IO 延迟,会把特征库缓存在进程内共享 Map 中。传统做法是直接修改这个 Map,多个 worker 线程可能在写入中途读到“半新半旧”的特征组合,导致模型预测漂移;停服重启又牺牲了可用性。利用 volatile 引用发布不可变特征快照,写线程先构建完整新快照再发布,读线程依据 volatile 写读的 Happens-Before 关系,必然拿到完整新版本。
具体案例:某推荐排序服务以 40 线程并发推理,每天零点切换特征版本。改造前偶发“一半新特征、一半旧特征”的串味预测;改为“不可变快照 + volatile 发布”后,版本切换零停服、零串味,A/B 实验的指标也不再被脏数据污染。
分布式训练中的指标采集与心跳
阶段:模型训练与可观测性
解决的问题:训练主线程不断更新 loss、吞吐量等指标,监控线程负责读取并上报看板。若两者之间没有 Happens-Before 通道,监控线程可能读到过期指标,把正常的 loss 波动误报成训练崩溃。用 volatile 字段发布最新 loss、用锁保护批量统计,即可让监控线程稳定读到同一批次的一致性数据。
具体案例:某大模型分布式训练任务中,监控线程曾周期性上报“loss 突跳”,经排查是主线程写入与监控读取之间缺少 HB 链导致的脏读。将指标改为 volatile 发布后,90 天训练周期内误报清零。
高并发 API 网关的限流计数器
阶段:在线服务治理
解决的问题:大模型 API 网关每秒处理海量 token 请求,限流计数器是典型的“多线程高竞争写”。用 synchronized 保护会引入锁竞争开销,且每次都要抢锁;用原子类 CAS 实现无锁自增,同时配合 volatile 定期发布计数快照给监控线程,既保证原子性又保证可见性。
具体案例:某 LLM 网关按 QPS 限流,改造前单机锁竞争导致 P99 延迟抖动 15%;改用 LongAdder 分段计数 + volatile 快照发布后,吞吐提升 20%,监控线程拿到的始终是完整的计数快照。
Python 演示
# -*- coding: utf-8 -*-"""happens-before 演示: threading.Event 建立线程间可见性秩序"""import threadingimport time class SauceShelf: """共享酱料架: 一个变量 + 一个传菜窗口(Event)""" def __init__(self): self.sauce_id = None # 共享变量: 酱料编号 self._window = threading.Event() # Event 自带内存屏障 def chef_a_write(self, recipe): self.sauce_id = recipe # 先写共享变量 self._window.set() # 再打开传菜窗口: 建立 happens-before def chef_b_read(self, timeout=5.0): got = self._window.wait(timeout) # 等窗口通知, 保证看见写 return self.sauce_id if got else None def broken_order(): shelf = SauceShelf() def a(): time.sleep(0.4) # A 需要 0.4 秒准备 shelf.sauce_id = 9527 # 写完就走, 没有通知 B def b(): time.sleep(0.2) # B 0.2 秒就来取 print("无同步 T2 读到:", shelf.sauce_id) # 极可能读到 None threading.Thread(target=a).start() threading.Thread(target=b).start() time.sleep(1.0) def fixed_order(): shelf = SauceShelf() def a(): time.sleep(0.4) shelf.chef_a_write(9527) # 先写, 再 set 通知 def b(): print("有HB T2 等到:", shelf.chef_b_read()) # 一定读到 9527 threading.Thread(target=a).start() threading.Thread(target=b).start() time.sleep(1.0) if __name__ == "__main__": print("=== 场景一: 没有 happens-before ===") broken_order() print("=== 场景二: Event 建立 happens-before ===") fixed_order()
threading.Event 建立 happens-before 通道,让读者稳定读到写者的新值。self._window = threading.Event()Event 内部使用锁与条件变量实现,自带内存屏障,天然是 Python 世界的“传菜窗口”。self.sauce_id = recipe先写普通共享变量,再打开窗口;set 之前的所有写,wait 之后都可见。self._window.set()打开传菜窗口,建立 happens-before:此前的写操作对等待者可见。got = self._window.wait(timeout)接菜人先等通知,保证读到完整新值,而不是靠 sleep 碰运气。time.sleep(0.4)模拟写线程的准备耗时,制造“读者过早到来”的时间差,让无同步场景的脏读稳定复现。print("无同步 T2 读到:", shelf.sauce_id)只靠 sleep 凑时序,B 在 A 写完前来取,读到的就是旧值 None。shelf.chef_a_write(9527)修复版把“写变量 + 开窗口”封装成一个动作,确保顺序正确。
常见误区
误区很多初学者觉得 volatile 变量自带“原子光环”,直接拿 `volatile int` 做多线程计数器自增,认为又可见又安全。
正解volatile 只保证可见性与有序性,不保证复合操作的原子性。i++ 在底层是“读-加-写”三步,volatile 只让每一步的结果可见,却阻止不了两个线程同时读出 1、又同时写回 2,导致丢更新。就像广播能让大家听见“新酱汁好了”,却不能让“盛酱→端走”这个动作不被另一位厨师打断。需要原子性,请用锁或原子类。
为何常见:volatile 语法极轻,一行关键字就有“同步感”,初学者容易把它脑补成万能同步器,忽略了“复合操作”这个前提。
误区有人认为 synchronized 或 Lock 的作用仅仅是“不让两个线程同时进临界区”,可见性问题还得靠 volatile。
正解锁同时在建立 happens-before 通道:解锁先行于后续同一把锁的加锁。线程 A 在锁内写的一切共享变量,在线程 B 拿到同一把锁后全部可见。就像冰箱钥匙——不只保证“一个时间只有一人进冰箱”,还保证后进冰箱的人能看到前人留下的全部取货痕迹。
为何常见:教科书常把锁讲成“互斥器”,弱化了它在内存语义上的效力,让人误以为互斥与可见性是两套割裂的机制。
误区“我跑了十万次都没出错,说明没有数据竞争。”这个信念在并发领域非常危险。
正解数据竞争属于未定义行为级别的问题,是否出错取决于编译优化路径、CPU 调度时序和缓存失效时机,和运行次数没有必然关系。就像厨房里偶尔打翻一次的酱油瓶,你站十小时没出事,不代表明天不洒。必须靠 happens-before 推理和检测工具确认,而不是靠运气。
为何常见:并发 bug 有极强的随机性和环境依赖性,普通测试很难恰好覆盖到触发崩溃的调度时序。
误区把 `thread.join()` 当成简单的“等待线程结束”,觉得它和 `Thread.sleep` 差不多,只是耗时间。
正解join() 除了等待,还自动建立 happens-before:被 join 线程内的所有写,对 join 返回之后的调用线程可见,不需要额外加锁。这是天然的跨线程交接通道。而 sleep 只是让出 CPU,不建立任何内存语义保证。
为何常见:join 的名字太“白话”,让人只盯着“等待”这个行为,忽略了它隐藏的内存屏障效应。
误区“我的服务器是单核 CPU,没有多核缓存不一致,所以不需要关心 happens-before。”
正解重排序不只来自 CPU 缓存,还有编译器优化。即便单核,编译器也可能调整指令顺序,操作系统也可能在任意指令边界切换线程,让读者读到“半成品”状态;JIT 的激进优化还会放大问题。内存模型约束的是“编译器 + 处理器 + 内存系统”三者,与核数无关。
为何常见:把内存模型问题片面理解为“多核缓存同步”,忽视了编译器和线程调度这两大重排来源。
面试题
Q1. happens-before 规则的核心作用是?
- A.提升多线程程序的运行速度
- B.保证操作的可见性与有序性← 正确答案
- C.确保线程不被系统打断
- D.让共享变量自动加锁
答案:B。Happens-Before 解决的是“谁先谁后、能否看见”:前序操作的结果对后续操作保证可见,且顺序不乱。它不保证提速,也不保证原子性,更不是给变量加锁。就像传菜窗口的交接规矩保证出菜人的手艺完整传给接菜人,而不是让传菜变快。
Q2. 下面哪个关键字能保证可见性但不保证原子性?
- A.synchronized 关键字
- B.ReentrantLock 锁对象
- C.volatile 关键字← 正确答案
- D.ThreadLocal 变量
答案:C。volatile 是“广播器”:写后立即刷入主内存,读时强制读主内存,保证可见性;但它不做互斥,i++ 这种读-改-写复合操作依然会被打断。synchronized 和 ReentrantLock 既保证互斥也建立 HB 通道;ThreadLocal 则是线程私有变量,不涉及共享。
Q3. 锁规则描述的是?
- A.加锁操作必须先于解锁操作
- B.解锁先行于同一锁后续加锁← 正确答案
- C.每个线程只能使用一把锁
- D.锁的粒度必须尽量最小
答案:B。锁规则是“解锁 happens-before 后续加锁”:线程 A 解锁前写的所有共享变量,对随后获取同一把锁的线程 B 可见。这好比冰箱钥匙的交接——后者能看到前人在冰箱里留下的全部痕迹。粒度最小化是性能建议,不是锁规则本身。
Q4. 线程 A 调用 B.start() 之后,以下哪个成立?
- A.B 的写操作对 A 全部可见
- B.A 中 start 前的写对 B 可见← 正确答案
- C.A 与 B 之间无法传递数据
- D.B 所有操作对 A 立即可见
答案:B。start 规则是“开火前的火候交接”:A 在调用 start 之前的所有写,对 B 线程内的动作可见;反向不成立,B 内写要对 A 可见需要 join 或其他同步手段。数据当然能传递,但必须借助同步机制。
Q5. 以下哪些机制可以建立 happens-before 关系?
- A.锁的解锁与后续加锁← 正确答案
- B.volatile 写与后续读← 正确答案
- C.普通变量的连续读取
- D.线程 join 方法返回← 正确答案
答案:ABD。锁的 unlock/lock、volatile 的写/读、线程的 start/join 都是官方钦定的 HB 通道。普通变量的读写不携带任何内存屏障,编译器可以任意重排或延迟刷新,因此 C 不能建立 HB 关系。
Q6. as-if-serial 语义指的是?
- A.所有线程看到完全一致的顺序
- B.单线程结果必须与顺序执行一致← 正确答案
- C.多线程必须严格串行访问共享
- D.禁止编译器做任何指令优化
答案:B。as-if-serial 是重排序的底线:无论编译器与 CPU 怎么折腾,单线程内最终结果必须与顺序执行完全一致。它只管“一个人的表演”,不管多线程间的可见性。跨线程的秩序要靠 Happens-Before 补足。
Q7. 下面哪种场景最适合使用 volatile?
- A.多线程计数器进行自增
- B.状态标志位单写多读场景← 正确答案
- C.替代锁保护临界区代码
- D.任意共享集合的并发修改
答案:B。volatile 是“广播”,适合一个线程写、多个线程读的状态标志,比如版本号、开关。计数器自增是复合操作,需要原子类或锁;临界区互斥和集合并发修改也都超出 volatile 的能力范围,贸然替代锁只会从“串味”变成“糊锅”。
Q8. 安全的对象发布方式包括?
- A.通过 volatile 引用发布← 正确答案
- B.通过 final 字段发布← 正确答案
- C.通过锁保护的字段发布← 正确答案
- D.无同步直接赋值静态字段
答案:ABC。安全发布就是“完整地把菜端上桌”:volatile 引用保证引用可见前对象已初始化;final 字段在构造完成后具备安全发布语义;锁保护的赋值借助 unlock/lock 的 HB 通道。无同步直接赋值可能让读者拿到“盖了半截章的对象”,属于不安全发布。
Q9. 数据竞争成立的必要条件有哪些?
- A.同一变量被多个线程访问← 正确答案
- B.至少存在一个写操作← 正确答案
- C.读写之间没有 HB 排序← 正确答案
- D.变量必须经过 volatile 修饰
答案:ABC。数据竞争 = 同一变量 + 多线程访问 + 至少一个写 + 无 HB 排序。两个线程同时读同一变量不算竞争;加了 volatile 反而是在建 HB 通道,是修复手段而非必要条件。数据竞争是未定义行为,不能靠“跑出来没出错”来排除。
Q10. 若 A 与 B 有 HB 关系且 B 与 C 有 HB 关系,则?
- A.A 与 C 之间一定有 HB 关系← 正确答案
- B.C 与 A 之间一定有 HB 关系
- C.A 与 C 之间不存在任何关系
- D.仅同一线程内传递才成立
答案:A。传递性是 Happens-Before 的公理之一:A→B→C 构成有序链,必然推出 A→C。B 就是那个“中间传菜人”,把 A 的成果接力传给 C,无论三者是否在同一线程。这一性质是设计无锁算法时最常用的推理工具。