春运抢票是每个人再熟悉不过的战役。设想一个售票大厅:起初只有一位人工售票员,所有乘客排成一条长队,一个人买完票,下一个人才能上前——速度慢、耗时长,一个人磨蹭,整个队伍都跟着遭殃。后来车站升级了:一整排无人售票机整装待命。没有队伍、没有叫号,你选中 5 号座位,机器在出票前的最后一刻再次核对——这个座位还空着吗?如果还空,立刻出票;如果刚被另一个人抢走,机器马上提示你重新选择,绝不会把同一张票卖给两个人。这个“再看一眼、立刻成交”的动作像闪电一样不可分割,计算机科学家把这套思路称为无锁编程,而这类不可分割的操作就是原子操作。当然,机器也有狡猾的漏洞:一个座位被买走又退掉,看起来“还是空的”,但中间早已换过主人——它还是原来那个它吗?这场抢票大赛里的博弈与智慧,正是无锁编程的全部奥秘。
掌握无锁编程的工程师,能在高并发系统中设计百万级吞吐的计数器、队列与状态机,从根源上规避锁竞争、阻塞与死锁,这是后端架构师与分布式系统工程师的核心竞争力。
可视化演示
展示无锁编程三大核心:CAS比较交换原理与重试机制、ABA问题与版本号解决方案、原子操作分类与无锁数据结构性能对比。切换模式查看各维度,拖动滑块调节并发线程数。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CAS比较交换原理 CAS(Compare-And-Swap)三步原子操作:读取当前值V→比较期望值E→若V==E则写入新值N并返回成功,否则返回失败并重试 多线程并发CAS:硬件级原子指令保证(x86 CMPXCHG/ARM LLSC),冲突时自旋重试,无阻塞但高竞争下吞吐下降 CAS(addr, expected, new) = atomic{ if(*addr==expected){*addr=new; return true} return false } CAS是无锁编程的基石
互斥锁局限
回到那个只有一个人工窗口的售票大厅。买票本身只要 30 秒,但队伍里有 100 个人时,第 50 号乘客要等上 25 分钟。更糟的是,队伍不能往前挪——每个人都必须站在原地排队,一旦离开队伍就要从头排起。程序里的互斥锁就是这支队伍:多个线程抢同一把锁,抢不到的线程被“罚站”进阻塞队列,操作系统必须暂停它、换上别的线程。
这一“罚站”与“换人”的过程叫线程上下文切换,一次切换就消耗数微秒,是普通语句开销的成百上千倍。锁竞争激烈时,大量 CPU 时间花在排队换人上,真正干活的时间反而少得可怜。这是锁的第一个代价:阻塞唤醒。
锁还有两个隐藏代价:死锁与优先级反转。两个线程各持有一把锁、又互相等待对方手里的锁,就会永远僵持,这就是死锁;低优先级线程拿着锁不放,高优先级线程被它阻塞,系统调度陷入混乱,这就是优先级反转。为避免死锁,程序员必须小心翼翼地设计加锁顺序;为避免反转,又要引入复杂的优先级继承机制。锁让并发程序从“正确性”到“性能”都变得难以掌控。
所以售票大厅越热闹,人工窗口越显得笨重——省下排队与叫号的成本,正是无锁编程存在的理由。
- T_critical(临界区执行时间)线程拿到锁、真正干活的时间,这通常很短。
- N_wait(等待线程数)排队等着抢锁的线程数量,竞争越激烈这个数越大。
- T_switch(上下文切换)操作系统暂停一个线程、换上另一个线程的调度开销。
- T_wake(唤醒开销)持锁线程释放锁后,把阻塞队列里睡眠的线程唤醒的开销。
CAS基本原理
车站终于装了无人售票机。它的聪明之处在于“最后核对一眼”:你选好 5 号座位,机器在出票的前一刻扫一眼屏幕——如果 5 号还显示“空”,就立刻出票并把状态改成“已售”;如果已经被人买走,机器当场报错,让你重新选,绝不把同一张票卖两次。这个“核对+修改”的动作,中间插不进任何其他操作。
在计算机里,这个动作叫 CAS(Compare-And-Swap,比较并交换)。它是一条由 CPU 硬件直接提供的原子指令:把一个内存地址里的当前值,与调用者提供的“期望值”比较;如果相等,就把新值写进去并返回成功;如果不相等,什么都不做并返回失败。这短短一条指令,是整个无锁编程世界的基石。
CAS 接受三个操作数:内存地址 p、期望值 e、新值 n。它解决的是一类经典并发难题——“读-改-写”三步操作被其他线程插队。常规的“读取变量 → 计算新值 → 写回变量”是三步,中间随时可能被别人改;而 CAS 把“确认没人改过 + 写入新值”压缩成一步。程序员通常把它包进一个自旋循环:先读旧值,算好新值,调用 CAS;失败说明别人抢先改了,于是重读、重算、再试,直到成功。
有了“最后核对”的机器,两个乘客同时抢同一个座位也不怕:总有一方的核对会通过,另一方则得到“已被购买”的提示,从头再选。并发世界第一次不用排队也能保证不出错。
- p(内存地址)共享变量在内存中的位置,相当于售票机屏幕上的“5 号座位”。
- e(期望值)调用方上一次读到的旧值,代表“我以为现在的状态”。
- n(新值)如果确认没人改过,将被写入 p 的目标值,相当于“已售”标记。
- 比较条件硬件将 *p 与 e 逐位比较,只有相等才允许交换,这是“最后核对”。
- 交换写入比较通过后,把 n 写入内存地址 p,完成“出票+改状态”。
- 原子性保障比较与写入由一条 CPU 指令完成,中间不可能被其他线程插入。
原子操作分类
售票大厅里不止一种“一键动作”:候补人数统计需要“自动加一”;余票显示屏需要“整屏刷新”;每天零点还要“等所有窗口的数据同步完,再统一放票”。这些动作的共同点是:要么不做,要做就一气呵成,中间不允许别人插手,这就是“原子性”。
从硬件指令的粒度看,原子操作可以分成几大家族:原子读/原子写(单条 load/store 指令天然原子);原子读改写 RMW(read-modify-write),例如“取出旧值并加一”的 Fetch-And-Add;比较交换 CAS;以及内存屏障(memory barrier)。内存屏障不直接修改数据,而是约束 CPU 和编译器打乱指令顺序,保证其他线程能看到“先发生的写”,相当于“等所有窗口同步完再放票”的闸门。
高级语言把这些硬件能力封装成原子类:Java 的 AtomicInteger、AtomicReference,C++ 的 std::atomic,Go 的 sync/atomic。以原子自增为例,incrementAndGet 底层就是 Fetch-And-Add 或 CAS 循环实现;compareAndSet 直接暴露 CAS;内存屏障则藏在各种“带序”的操作里(如 C++ 的 memory_order_acquire/release)。
有了“自动加一”的按钮,候补人数再多、点得再快也不会漏记。原子操作就是把那些最容易被插队的动作,全部做成了“一键执行”。
- 读旧值先把内存 p 的当前值保存到局部变量 old,相当于“看一眼当前计数”。
- 加 v在旧值基础上增加 v,这一计算发生在 CPU 内部,不落内存。
- 写回将加完的新值写回内存地址 p,与前面的读合并在一条指令里。
- 返回旧值把最初的 old 返回给调用方,方便用它做“取号”等操作。
ABA问题
现在假设 5 号座位在 12:00 显示“空”。小明看到后开始下单,下单需要 2 秒。在这 2 秒里,小红买走了 5 号座,随后又因行程改变退了票——12:00:01,5 号座重新显示“空”。小明的订单在 12:00:02 核对时发现“座位还是空的”,于是出票成功。可这张票真的是他看到的那个“原装”座位吗?显然不是:中间它被另一个乘客买走过、退掉过,票务状态早已不是原来那个干净的样子。
这就是 ABA 问题:一个共享变量的值从 A 变成 B,又变回 A。第二次变回 A 后,数值与最初完全一样,因此基于“值比较”的 CAS 会误判“没人动过”。但在两次变化之间,系统状态可能已被彻底改写——尤其对无锁数据结构(如无锁栈),头指针 A 被弹出又压回,CAS 会把一个早已失效的节点当作当前节点,造成数据丢失或链表错乱。
解决 ABA 的标准手段是“带版本的比较”:不再只比较值,而是比较“值+版本号”的复合结构。每次修改,值在变的同时版本号也自增。即使值绕了一圈变回 A,版本号也不可能退回,CAS 就能识破“此 A 非彼 A”。Java 提供了 AtomicStampedReference(携带标记的原子引用),C++ 可在原子指针旁附带计数器;对于容易复用的内存节点,还可以用延迟释放(hazard pointer)保证“旧 A”永远不会被重新利用。
就像售票系统不仅查“5 号座是否为空”,还要核对该座位的“班次版本”——退过票的座位,标记已经更新,系统一眼就能看出它不是原来那个 A。
- 旧状态元组线程读取时记录下(值 A,版本号 v),这是“我看到的是第 v 代的 5 号座”。
- 新状态元组CAS 被调用时,内存中的真实状态是(值 A,版本号 v')。
- 版本比较当 v ≠ v' 时,说明这个 A 已经历过至少一次完整变化,拒绝交换。
- 版本一致才交换只有当版本号也没变,才真正说明“无人动过”,允许写入新值。
无锁数据结构
售票大厅里最好的消息是:车站开了一整排无人售票机。没有队伍,没有叫号,任何乘客任何时候都可以走到任意一台机器前操作。更妙的是,机器们有一个庄严的承诺:只要大厅里还有人打算买票,就一定有某个人能成功买到票——绝不会因为某台机器卡住,就导致全场瘫痪。
这个承诺对应无锁编程的核心定义:lock-free(无锁)算法保证“系统整体必然推进”——任意时刻,至少有一个线程的操作能成功完成。与之相对,wait-free(无等待)更进一步,保证每个线程都在有限步内完成。基于 CAS 自旋循环,经典构件包括:Treiber 栈(用 CAS 更新栈顶)、Michael-Scott 队列(用 CAS 更新队头队尾)、以及各种无锁链表和哈希表。
以 Treiber 栈的 push 为例:读当前栈顶 top,把新节点的 next 指向它,然后 CAS(top, 旧top, 新节点)。如果 CAS 失败,说明别人刚改过栈顶,循环重来。整个过程中没有一把锁,线程永远不会被阻塞;而 CAS 的“最后核对”保证了栈顶绝不会被两个线程同时覆盖。代价是:无锁数据结构往往要处理 ABA、内存回收(节点被弹出后不能立刻释放)、以及更高的实现复杂度。
无人售票机阵列永远不“全场清空”:某台机器忙、某位乘客手慢,都不影响别人买到票。无锁数据结构就是把这套承诺搬进了内存——高并发下依然人人有票。
- 读栈顶 old把当前 top 指针读到局部变量,这是“看一眼现在谁在最上面”。
- 新节点指向 old把新节点的 next 链到刚读到的栈顶,做好插入准备。
- CAS 核对入栈如果 top 仍是 old,说明没人抢先,把 top 改成 n,入栈成功;否则说明别人改了栈顶,回到第 1 步重试。
- 循环直至成功整个流程在自旋中反复,但线程始终处于运行态,永不阻塞。
性能评估
无人售票机真的永远比人工窗口快吗?不一定。早晨 6 点,大厅里只有两三个人,机器“即到即办”,完胜排队窗口;可到了抢票高峰,上千人同时涌向机器,每个人都在飞速点击“查询-下单-失败-重试”,机器的核对请求彼此冲突,屏幕上全是“已被购买”,大厅反而比排队更乱。
无锁编程的性能曲线正是如此:低竞争、短临界区时,没有阻塞唤醒开销,无锁方案干净利落;高竞争时,大量线程的 CAS 失败重试反而造成 CPU 空转,同时多核反复改写同一缓存行会触发 MESI 缓存一致性协议(多核之间同步数据修改的规则)的大量同步流量——这就是“缓存行乒乓”。此时精心调优的锁(如自适应自旋锁)可能更稳定。
因此,无锁不是银弹,选择时有三条准则:第一,临界区极短且操作频繁,优先无锁(计数器、状态切换);第二,操作涉及 IO、网络或耗时计算,绝不能自旋死等,必须用锁或异步;第三,多写者竞争同一个变量时,考虑分片(如 LongAdder 把计数器拆成多个子计数,读时合并),或引入指数退避,让重试前随机等一小段时间,降低碰撞。
聪明的大厅会动态分流:人少时全开机器,人挤时给机器加“退避”(点太快就随机停半秒),太极端时干脆临时开回人工窗口排队。性能评估的本质,是知道什么时候该依赖机器,什么时候该依赖纪律。
- 单次失败概率 p_fail一次 CAS 因值被改而失败的概率,它随竞争线程数升高。
- 重试次数期望几何分布的均值,失败率 p_fail 时平均要尝试 1/(1-p_fail) 次。
- 单次 CAS 耗时 T_cas一次比较交换指令的花费,通常只有几纳秒。
- 总重试代价失败率越接近 1,分母越小,总时间成倍放大,这就是高竞争下无锁退化的数学根源。
工程实践
把目光从售票大厅拉回机房:每天都在运转的互联网服务,其实就是一座庞大的“抢票大厅”。用户点一次赞、发一条消息、提交一个训练任务,背后都有成千上万的线程在并发读写共享状态。而工程师们的兵器库里,最趁手的就是那些“一键原子操作”。
最常见的实战形态是原子计数器:QPS 统计、在线人数、候补队列长度、全局自增 ID、训练样本计数……它们本质都是“取当前值、加一、写回”,用原子类实现后,百万 QPS 下也不会丢一个累加。第二种形态是无锁状态机:用 CAS 保证“只有当状态是 READY 时才允许变为 RUNNING”,多线程竞争状态迁移时,谁先 CAS 成功谁说了算,自然避免重复初始化。
语言层面的支持非常成熟:Java 的 AtomicInteger / LongAdder / AtomicStampedReference,Go 的 sync/atomic,C++ 的 std::atomic。但使用时有一个工程要点:内存序(memory order)——C++ 需要显式声明 relaxed/acquire/release 等顺序,Java 的 volatile 与原子类也各有语义;二是伪共享(false sharing)——多个原子变量挤在同一缓存行时会被无谓拖累,需要 padding 对齐;三是调试困难,建议配合 ThreadSanitizer、模型检查工具和压测平台验证正确性。
从候补计数到状态切换,售票大厅的每一次“毫秒级刷新”背后,都是无数个原子操作在无声奔跑。理解它们,你就能在系统设计的图纸上,画出那个永不排队的未来。
- 当前状态共享变量 state 保存任务的进度,例如就绪 READY。
- CAS 推进尝试把 READY 切换为 RUNNING,同时要求内存里当前值仍是 READY。
- 成功分支CAS 成功说明本线程抢到了唯一推进权,状态更新为 RUNNING。
- 失败分支CAS 失败说明另一线程已完成推进,本线程什么都不用做,直接返回。
AI 应用场景
AI推理网关的高并发限流与指标统计
阶段:模型部署与在线推理
解决的问题:传统做法用互斥锁保护计数器和令牌桶,每次请求都要抢锁,在高并发下锁竞争浪费大量 CPU,甚至引发线程阻塞,直接影响推理延迟。改用无锁原子操作后,QPS 统计、在线请求数、限流令牌的抢占都变成一条 CAS/原子自增指令,统计与限流几乎零成本。
具体案例:某大模型 API 网关每秒接收 2 万次请求,多个 worker 线程并发处理。网关用 LongAdder/AtomicLong 记录每秒请求数,用 CAS 尝试从令牌桶中“取令牌”:取出成功就放行,失败直接返回 429 限流。升级无锁后,网关 P99 延迟降低约 30%。
无锁任务队列驱动批量推理
阶段:推理调度与批处理
解决的问题:大语言模型服务通常把多个请求拼成一个 batch 交给 GPU 计算。生产者线程把 prompt 放入队列,消费者线程取出拼接 batch。如果用互斥锁队列,入队出队的高频竞争会成为吞吐瓶颈。
具体案例:基于 Michael-Scott 无锁队列维护“待推理请求缓冲”,多个前端线程同时入队,GPU 调度线程批量出队。无锁队列保证任何时刻至少有一个线程能完成入队/出队,整体吞吐提升约 40%,且高负载下不会出现“全员阻塞”的雪崩。
分布式训练数据的样本状态分配
阶段:数据流水线与训练调度
解决的问题:多个数据加载进程并发领取训练样本,要避免同一份样本被两个 worker 重复训练,同时不能因为加锁拖慢整个数据流水线。
具体案例:样本记录带状态字段 pending → in-training → done。每个数据加载 worker 用 CAS 把样本从 pending 推进到 in-training:CAS 成功者获得该样本的唯一使用权,失败者立刻换下一份,无需加锁也不产生竞争等待。类似的思路还可用于模型权重版本切换、checkpoint 状态机。
Python 演示
import threading class AtomicInt: """模拟硬件原子整数, 内部加锁只为模拟单条原子指令""" def __init__(self, value=0): self._mu = threading.Lock() self._value = value def load(self): with self._mu: return self._value def cas(self, expected, new): with self._mu: if self._value == expected: self._value = new return True return False def lock_free_increment(cnt, times): """基于 CAS 的无锁自增: 失败就重读重试, 全程不拿互斥锁""" for _ in range(times): while True: old = cnt.load() if cnt.cas(old, old + 1): break def counter_demo(): n_threads, n_times = 8, 5000 cnt = AtomicInt(0) threads = [ threading.Thread(target=lock_free_increment, args=(cnt, n_times)) for _ in range(n_threads) ] for t in threads: t.start() for t in threads: t.join() expect = n_threads * n_times actual = cnt.load() print("期望值:", expect, "实际值:", actual, "结果:", expect == actual) def cas(ptr, expected, new): if ptr["node"] == expected: ptr["node"] = new return True return False def aba_demo(): print("=== 无版本号: 值 A -> B -> A, CAS 被骗 ===") ptr = {"node": "A"} old = ptr["node"] # 线程1读旧值 cas(ptr, "A", "B") # 线程2: A->B cas(ptr, "B", "A") # 线程2: B->A print("线程1 CAS(A->C):", cas(ptr, old, "C"), " head =", ptr["node"]) print("=== 带版本号: 版本已变, CAS 识破 ABA ===") ptr_v = {"node": "A", "ver": 0} old_node, old_ver = ptr_v["node"], ptr_v["ver"] cas(ptr_v, "A", "B") ptr_v["ver"] += 1 cas(ptr_v, "B", "A") ptr_v["ver"] += 1 same = (ptr_v["node"] == old_node) and (ptr_v["ver"] == old_ver) print("版本号一致?", same, "-> 拒绝按老版本更新") if __name__ == "__main__": counter_demo() aba_demo()
if self._value == expected:比较内存当前值与期望值,这是 CAS 的“核对”步骤。self._value = new核对相等才交换,把新值写入,等价于“出票改状态”。old = cnt.load()自旋循环第一步:读取当前值作为期望值。if cnt.cas(old, old + 1):尝试把 old 换成 old+1,失败就回到 while 重读重试。threading.Thread(target=lock_free_increment, args=(cnt, n_times))8 个线程同时操作同一个原子变量。ptr = {"node": "A"}用字典模拟共享指针,ABA 演示的核心对象。ptr_v["ver"] += 1每次修改都让版本号自增,这是识破 ABA 的关键。same = (ptr_v["node"] == old_node) and (ptr_v["ver"] == old_ver)值相同但版本不同,判定“不是原来那个 A”。
常见误区
误区CAS 是原子操作,所以基于 CAS 写的并发代码天然线程安全,永远不会出 bug。
正解原子性只保证“一次比较+交换”不可分割,不代表整个“读-改-写”流程的逻辑安全。ABA 问题、内存乱序导致的可见性问题、自旋重试的饥饿,都会让基于 CAS 的代码出错。类比售票机:它保证“核对+出票”一步到位,但如果你一开始就看错了车次(读到了过期值),照样买错票。
为何常见:“原子”这个词听起来像“绝对安全”,初学者容易把硬件指令的原子性误当成整个并发流程的原子性。
误区i++ 就是原子操作,多线程自增不需要额外处理。
正解i++ 编译后是“读取、加一、写回”三条指令,任何两条之间都可能被别的线程插队。两个线程同时对同一个 i 执行 i++,最终可能只加了一次。真正原子的是硬件指令(Fetch-And-Add)或语言原子类(AtomicInteger.incrementAndGet)。
为何常见:高级语言里 i++ 只占一行,看起来像一条不可分割的语句,容易忽略它底层的多条指令。
误区无锁编程不会让线程睡觉,所以性能一定比锁好。
正解这只在低竞争、短临界区时成立。高竞争下 CAS 大量失败,线程自旋空转吞噬 CPU;多核反复改写同一缓存行还会触发缓存一致性协议的同步风暴,性能可能比精心调优的锁更差。选择前要评估竞争强度和临界区长度。
为何常见:人们记住了“无锁=不阻塞”的优点,却忽略了“自旋重试+缓存同步”这两个隐性成本。
误区ABA 问题只是理论玩具,生产环境里根本不会发生。
正解只要对象内存会被复用——比如弹出的节点进入对象池或被 GC 后重新分配——无锁栈、无锁队列就可能在真实负载下触发 ABA,表现为节点丢失、链表断裂甚至数据错乱。必须用版本号、AtomicStampedReference 或延迟释放来防御。
为何常见:ABA 需要精确的时序才能稳定复现,日常调试中很难肉眼观察到,让人误以为它不存在。
误区无锁编程就是 wait-free,每个线程永远都不会等待。
正解无锁(lock-free)只承诺“系统整体必然推进”:任意时刻至少有一个线程能成功完成操作;单个线程在极端竞争下可能反复失败、无限重试(饥饿)。更强的是 wait-free(无等待),保证每个线程都在有限步内完成,但实现成本极高。
为何常见:字面上“无锁”让人联想到“无等待”,混淆了 lock-free 与 wait-free 两个不同强度的并发保证。
面试题
Q1. CAS(Compare-And-Swap)的核心动作是?
- A.比较-扩散-同步
- B.比较-交换-写入← 正确答案
- C.复制-粘贴-删除
- D.检查-锁定-释放
答案:B。CAS 先比较内存当前值与期望值,相等才把新值写入,即“比较+交换+写入”压缩成一条指令。A 的“扩散”、C 的“复制粘贴”、D 的“锁定”都不是 CAS 的语义,属于混淆选项。
Q2. CAS 指令的原子性由谁保证?
- A.操作系统调度器
- B.应用层业务代码
- C.硬件 CPU 指令← 正确答案
- D.数据库事务提交
答案:C。CAS 是 CPU 提供的原子指令(如 x86 的 CMPXCHG),比较与写入在指令级不可分割。操作系统调度和应用代码只能做粗粒度的同步,无法保证单条读-改-写指令的原子性。
Q3. 内存屏障(memory barrier)的主要作用是?
- A.阻止指令乱序与重排← 正确答案
- B.提高线程的优先级
- C.替换全部的原子类
- D.自动回收无用内存
答案:A。内存屏障是约束 CPU 与编译器指令重排的栅栏,保证屏障前的读写不会跑到屏障后,解决多核场景下的可见性问题。它和线程优先级、原子类替换、内存回收都没有关系。
Q4. 自旋重试的 CAS 循环最适合哪种场景?
- A.临界区涉及网络 IO
- B.临界区短且竞争低← 正确答案
- C.临界区可能长时间阻塞
- D.线程数远远超过核心数
答案:B。自旋会空转 CPU,只适合“马上就能成功”的短临界区。涉及 IO 或长时间阻塞的临界区会让自旋白白烧 CPU,应该用锁或异步;等待线程过多时同样不适合自旋。
Q5. 下列关于 ABA 问题描述正确的是?
- A.值A变B后,线程死循环
- B.值A与B完全相同,属正常
- C.值A变B又回A,CAS误判未变← 正确答案
- D.值A变B时,线程触发死锁
答案:C。ABA 指共享值从 A 变为 B 再变回 A,以“值相等”为判断依据的 CAS 会误以为“无人改动”,从而基于过期信息做错误操作。死循环、死锁都不是 ABA 的典型表现。
Q6. 解决 ABA 问题最常用的方法是?
- A.附加单调递增的版本号← 正确答案
- B.增大自旋重试次数
- C.让线程交替休眠
- D.增加更多的共享变量
答案:A。版本号让比较对象变成“值+版本号”,值虽回到 A 但版本号已经递增,CAS 拒绝交换。增大自旋次数、线程休眠、增加变量都无法识别“此 A 非彼 A”。
Q7. Treiber 无锁栈的 pop 操作,CAS 比较的对象是?
- A.栈顶指针的当前值← 正确答案
- B.栈内元素的总数量
- C.当前线程的优先级
- D.节点数据的内存空间
答案:A。pop 先读栈顶 old,再 CAS(top, old, old.next),比较的是栈顶指针是否仍为 old;只有没人抢先修改栈顶,才把 top 更新为下一个节点。这是无锁栈正确性的关键。
Q8. 高竞争下无锁性能退化的原因有哪些?
- A.失败CAS引发大量重试← 正确答案
- B.缓存一致性协议流量激增← 正确答案
- C.数据结构逻辑必然会崩溃
- D.自旋空转白占 CPU 周期← 正确答案
答案:ABD。高竞争时 CAS 失败率高,线程自旋重试浪费 CPU;多核竞争同一缓存行触发 MESI 一致性流量,导致性能雪崩。但正确实现的无锁数据结构不会必然崩溃,它仍保证系统整体推进。
Q9. 以下哪些属于原子操作?
- A.普通整型变量的自增
- B.原子引用类的比较交换← 正确答案
- C.加锁保护的复杂事务
- D.原子整数类的自增方法← 正确答案
答案:BD。原子引用类的比较交换与原子整数类的自增方法都由硬件原子指令实现;普通 i++ 是“读-加-写”三步非原子;加锁保护的事务依赖锁机制而不是原子指令,都不属于原子操作。
Q10. 哪些场景适合采用无锁编程?
- A.高频短临界区的计数器← 正确答案
- B.长时间持锁访问数据库
- C.高频任务入队的批处理← 正确答案
- D.无锁状态机的状态切换← 正确答案
答案:ACD。无锁适合“短、快、高频”的操作:计数器累加、任务入队、状态切换。长时间持锁访问数据库属于慢临界区,自旋只会浪费 CPU,应使用锁、连接池或异步化。