2.13 加锁还是不加锁,这是一个问题
沿旧值读取、新值计算、CAS 更新、失败检查和退避重试追踪无锁更新,用 ABA 与竞争实验解释原子性和进展保证的边界。
学习目标
- 能沿读取旧值、计算新值、执行CAS、检查失败和退避重试追踪一次条件原子更新
- 能区分互斥、原子性、无锁和有限等待,解释为什么 CAS 成功不等于整个业务不变量自动成立
- 能在正常、竞争和 ABA 故障场景中定位首个偏离,选择加锁、版本标记或重试策略并重放
为什么需要这一机制
共享状态更新既可以用互斥锁串行化,也可以用 CAS 在条件满足时原子替换。CAS 的吸引力是失败线程不必一直占有锁,但它把正确性责任转移给读取、计算、比较、重试和不变量设计。无锁也不等于没有等待,更不等于每个线程都能在有限时间内完成。
核心合同
↡本页把 CAS 重构为:只有内存中的当前值仍等于期望值时,才原子写入新值;失败者必须重读并按策略重试或退出。式子只描述一个地址上的条件更新。业务正确性还需要共享不变量、内存可见性、重试上限、退避和公平性证据。一次成功 CAS 不能替代整个临界区的设计。
四个官方概念到机制证据
2.13 加锁还是不加锁,这是一个问题
↡围绕互斥与 CAS 的取舍,分析共享更新在原子性、阻塞、进展和不变量之间的同步设计问题。它不是二选一的口号,而是先写出不变量、竞争模型和进展目标,再选择锁或条件原子更新。保存线程事件、线性化点、失败次数和最终状态。
互斥锁
↡用互斥进入临界区,把共享状态的读取、计算和写入串行化,以换取更直接的整体不变量保护。互斥锁可降低组合状态的推理复杂度,但仍需观察锁范围、持有时间、死锁顺序和等待分布。加锁不是免费正确,也不是所有场景都应避免。
要不要加锁
↡根据共享不变量、竞争强度、阻塞容忍度和进展保证选择互斥、CAS 或混合策略的决策节点。先问更新是否只涉及一个可独立替换的状态,再问失败重试是否有预算和公平性要求。若整体不变量跨多个对象,单字段 CAS 往往不足。
CAS的扩展
↡在基础比较交换上加入版本标记、复合状态、退避和失败重试策略,以处理 ABA、竞争和更复杂的共享更新。扩展不是把循环写得更快,而是把状态版本、失败原因和完成保证写进合同。记录每次重读、比较、失败和退避,才能评估系统级进展。
五个节点到机制证据
读取旧值
↡在线程计算前读取共享地址的当前值、版本或复合状态,并形成本次 CAS 的期望值。保存读取时刻、线程 ID、地址/键、值和版本。只读裸数值会让 ABA 变化在证据中消失。
计算新值
↡根据旧值、输入和业务不变量计算候选新值,同时不修改共享状态。计算过程应是可重放的;若依赖多个共享字段,要明确它们是否被同一个同步合同保护。
执行CAS
↡原子比较当前值与期望值,匹配时写入新值并报告成功,不匹配时保持共享状态并报告失败。记录比较时的实际值、期望值、新值、线程和线性化结果。成功只证明这个比较交换点成立。
检查失败
↡读取 CAS 的失败结果并区分竞争、版本变化、ABA 风险或不再满足业务条件的原因。失败不是异常噪声,而是共享状态变化的证据。重试前要重新读取,达到上限或业务条件失效时必须退出并报告。
退避重试
↡在 CAS 失败后按次数、时间或队列策略延迟并重新读取,控制竞争放大和线程饥饿。记录退避次数、延迟、成功率和单线程最长等待。需要 wait-free 保证时,普通退避循环不能冒充该保证。
最小可重放实现
old = read(addr, version)
next = compute(old, input)
if CAS(addr, old, next):
return committed
recordFailure(old, next)
backoff(attempt)
retryWithinBudget()这段草图只表达条件原子更新合同,不复制书中叙事或代码。实际复核应保存线程、旧值/版本、新值、CAS 结果、失败原因、退避和最终不变量。
五步复核一次 CAS 更新
1. 固定共享状态和不变量
记录共享地址或复合状态、业务输入、版本、线程数和完成目标;先预测无竞争基线和允许的最终状态。
Lab
CAS 竞争、退避与 ABA 实验
一次只改变竞争、版本标记或重试预算,观察原子性与进展边界。
单线程读取旧值后一次 CAS 成功
read A(v1) → compute B → CAS(A(v1),B)=success → invariant ok
判定
通过:线性化点、状态版本和最终不变量一致
当前场景:基线成功;记录旧值、版本、新值、CAS 结果、失败次数、退避、最长等待和复位。
正常、边界与故障证据
| 场景 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 单线程或低竞争,版本连续 | CAS 成功,不变量成立,轨迹可重放 | 旧值、版本、新值、线程 |
| 边界 | 高竞争、失败预算或长退避 | 失败可解释,系统保持不变量,公平性目标明确 | 失败次数、延迟、最长等待 |
| 故障 | A→B→A 或跨字段更新被拆开 | 版本识别 ABA 或拒绝错误更新 | 状态序列、版本、线性化点、复位 |
专属因果实验
先运行无竞争基线,预测五个节点的事件顺序;再一次只增加竞争线程、改变版本标记或缩短重试预算。实验显示旧值、版本、CAS 成功/失败、退避次数和最终不变量,避免把“没有锁”误当成“没有等待”。
Lab
CAS 竞争、退避与 ABA 实验
一次只改变竞争、版本标记或重试预算,观察原子性与进展边界。
单线程读取旧值后一次 CAS 成功
read A(v1) → compute B → CAS(A(v1),B)=success → invariant ok
判定
通过:线性化点、状态版本和最终不变量一致
当前场景:基线成功;记录旧值、版本、新值、CAS 结果、失败次数、退避、最长等待和复位。
故障诊断:沿失败事件找首个竞争偏离
- 核对状态读取:比较每个线程读取的值、版本和时间,寻找被其他线程改变的首个点。
- 核对候选计算:确认计算只基于已读状态,检查跨字段不变量是否被拆开。
- 核对 CAS 结果:保存期望值、实际值和线性化点,识别普通竞争与 ABA。
- 核对进展与复位:检查失败预算、退避、公平性和最终不变量,从空状态重放。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 2.13 加锁还是不加锁,这是一个问题
围绕互斥、CAS、阻塞、原子性和进展保证的同步设计问题。
- 互斥锁
通过串行进入临界区保护组合共享不变量的同步机制。
- 要不要加锁
根据不变量、竞争和进展目标选择同步策略的决策节点。
- CAS的扩展
加入版本、复合状态、退避和重试策略的条件原子更新方案。
- 读取旧值
读取共享状态并形成比较交换期望值的阶段。
- 计算新值
基于旧状态和输入产生候选更新且不修改共享状态的阶段。
- 执行CAS
在期望值匹配时原子写入新值并报告结果的阶段。
- 检查失败
解释 CAS 未匹配原因并决定重读、退出或报告的阶段。
- 退避重试
通过延迟和预算控制竞争放大、饥饿与等待的阶段。
练习
练习
问题 1(2.13 加锁还是不加锁,这是一个问题、互斥锁): 什么时候单字段 CAS 不足以保护业务操作?
问题 2(要不要加锁): 为什么“无锁”不等于“不会等待”?
问题 3(CAS的扩展): 如何用版本标记识别 ABA?
本页小结
2.13 加锁还是不加锁,这是一个问题的关键不是宣称锁或 CAS 永远更好,而是沿读取旧值、计算新值、执行CAS、检查失败和退避重试保存原子性、版本和进展证据。完成标准是识别组合不变量、ABA 和饥饿边界,在首个偏离处选择同步策略并用清空状态后的重放验证。