2.13 加锁还是不加锁,这是一个问题

沿旧值读取、新值计算、CAS 更新、失败检查和退避重试追踪无锁更新,用 ABA 与竞争实验解释原子性和进展保证的边界。

学习目标

  • 能沿读取旧值、计算新值、执行CAS、检查失败和退避重试追踪一次条件原子更新
  • 能区分互斥、原子性、无锁和有限等待,解释为什么 CAS 成功不等于整个业务不变量自动成立
  • 能在正常、竞争和 ABA 故障场景中定位首个偏离,选择加锁、版本标记或重试策略并重放

为什么需要这一机制

共享状态更新既可以用互斥锁串行化,也可以用 CAS 在条件满足时原子替换。CAS 的吸引力是失败线程不必一直占有锁,但它把正确性责任转移给读取、计算、比较、重试和不变量设计。无锁也不等于没有等待,更不等于每个线程都能在有限时间内完成。

核心合同

CAS(addr,expected,new)old=expectedCAS(addr,expected,new)\Rightarrow old=expected

式子只描述一个地址上的条件更新。业务正确性还需要共享不变量、内存可见性、重试上限、退避和公平性证据。一次成功 CAS 不能替代整个临界区的设计。

CAS 更新链:成功点之外还要证明进展旧值、版本、失败原因和退避策略共同解释一次共享更新1读取旧值期望 + 版本线程事件2计算新值候选状态线程事件3执行CAS原子比较线性化点4检查失败竞争/ABA故障证据5退避重试预算与进展线程事件CAS 成功只证明比较交换点成立,不自动证明组合不变量与公平性
专属图示:把旧值、线性化点、ABA 证据和进展策略放进一条轨迹。

四个官方概念到机制证据

2.13 加锁还是不加锁,这是一个问题

它不是二选一的口号,而是先写出不变量、竞争模型和进展目标,再选择锁或条件原子更新。保存线程事件、线性化点、失败次数和最终状态。

互斥锁

互斥锁可降低组合状态的推理复杂度,但仍需观察锁范围、持有时间、死锁顺序和等待分布。加锁不是免费正确,也不是所有场景都应避免。

要不要加锁

先问更新是否只涉及一个可独立替换的状态,再问失败重试是否有预算和公平性要求。若整体不变量跨多个对象,单字段 CAS 往往不足。

CAS的扩展

扩展不是把循环写得更快,而是把状态版本、失败原因和完成保证写进合同。记录每次重读、比较、失败和退避,才能评估系统级进展。

五个节点到机制证据

读取旧值

保存读取时刻、线程 ID、地址/键、值和版本。只读裸数值会让 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 / 5

1. 固定共享状态和不变量

记录共享地址或复合状态、业务输入、版本、线程数和完成目标;先预测无竞争基线和允许的最终状态。

Lab

CAS 竞争、退避与 ABA 实验

一次只改变竞争、版本标记或重试预算,观察原子性与进展边界。

单线程读取旧值后一次 CAS 成功

read A(v1) → compute B → CAS(A(v1),B)=success → invariant ok

判定

通过:线性化点、状态版本和最终不变量一致

当前场景:基线成功;记录旧值、版本、新值、CAS 结果、失败次数、退避、最长等待和复位。

正常、边界与故障证据

并发更新证据矩阵原子性、进展和业务不变量需要分别验收观察项正常边界故障期望匹配过期ABA更新一次成功多次失败错写进展快速退避饥饿不变量成立需锁破坏失败次数、版本和最终不变量一起判断一次无锁更新是否可接受
专属图示:分别验收比较条件、更新结果、线程进展和共享不变量。
场景只改变的变量预期判定必存证据
正常单线程或低竞争,版本连续CAS 成功,不变量成立,轨迹可重放旧值、版本、新值、线程
边界高竞争、失败预算或长退避失败可解释,系统保持不变量,公平性目标明确失败次数、延迟、最长等待
故障A→B→A 或跨字段更新被拆开版本识别 ABA 或拒绝错误更新状态序列、版本、线性化点、复位

专属因果实验

先运行无竞争基线,预测五个节点的事件顺序;再一次只增加竞争线程、改变版本标记或缩短重试预算。实验显示旧值、版本、CAS 成功/失败、退避次数和最终不变量,避免把“没有锁”误当成“没有等待”。

Lab

CAS 竞争、退避与 ABA 实验

一次只改变竞争、版本标记或重试预算,观察原子性与进展边界。

单线程读取旧值后一次 CAS 成功

read A(v1) → compute B → CAS(A(v1),B)=success → invariant ok

判定

通过:线性化点、状态版本和最终不变量一致

当前场景:基线成功;记录旧值、版本、新值、CAS 结果、失败次数、退避、最长等待和复位。

故障诊断:沿失败事件找首个竞争偏离

  1. 核对状态读取:比较每个线程读取的值、版本和时间,寻找被其他线程改变的首个点。
  2. 核对候选计算:确认计算只基于已读状态,检查跨字段不变量是否被拆开。
  3. 核对 CAS 结果:保存期望值、实际值和线性化点,识别普通竞争与 ABA。
  4. 核对进展与复位:检查失败预算、退避、公平性和最终不变量,从空状态重放。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

2.13 加锁还是不加锁,这是一个问题

围绕互斥、CAS、阻塞、原子性和进展保证的同步设计问题。

互斥锁

通过串行进入临界区保护组合共享不变量的同步机制。

要不要加锁

根据不变量、竞争和进展目标选择同步策略的决策节点。

CAS的扩展

加入版本、复合状态、退避和重试策略的条件原子更新方案。

读取旧值

读取共享状态并形成比较交换期望值的阶段。

计算新值

基于旧状态和输入产生候选更新且不修改共享状态的阶段。

执行CAS

在期望值匹配时原子写入新值并报告结果的阶段。

检查失败

解释 CAS 未匹配原因并决定重读、退出或报告的阶段。

退避重试

通过延迟和预算控制竞争放大、饥饿与等待的阶段。

练习

练习

问题 1(2.13 加锁还是不加锁,这是一个问题、互斥锁): 什么时候单字段 CAS 不足以保护业务操作?

问题 2(要不要加锁): 为什么“无锁”不等于“不会等待”?

问题 3(CAS的扩展): 如何用版本标记识别 ABA?

资料与写作方式声明

本章以码农翻身权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本页小结

2.13 加锁还是不加锁,这是一个问题的关键不是宣称锁或 CAS 永远更好,而是沿读取旧值、计算新值、执行CAS、检查失败和退避重试保存原子性、版本和进展证据。完成标准是识别组合不变量、ABA 和饥饿边界,在首个偏离处选择同步策略并用清空状态后的重放验证。

讨论

评论区加载中…