Sunday面试指南

CAS 是什么?ABA 问题怎么产生,为什么加版本号能解决?

下面是一段教学用的模拟面试。

🧑‍💻 面试官:CAS 为什么能避免两个线程同时覆盖一个计数?

🙋‍♂️ 我:先比较旧值,如果没变,就更新成新值。

🧑‍💻 面试官:比较和更新中间,另一个线程改了怎么办?

🙋‍♂️ 我:真正的 CAS 要把比较和更新作为一次原子操作,不能由普通 if 和赋值拼起来。

🧑‍💻 面试官:如果旧值从 A 变成 B,又变回 A,比较不是仍然成功?那能证明它从来没被动过吗?

CAS 核对的是「现在是不是我预期的值」。如果还要知道中间有没有变化,就得再带上版本。

面试速答(60 秒版)

CAS 是比较并交换:在一次原子操作中,比较当前位置与预期值;相同才更新,否则失败。它可以帮助实现单变量的条件更新,但普通读取、if 判断和赋值并不等于 CAS。

常见做法是读取旧值、计算新值、尝试 CAS;失败后重新读取再计算。这样可以避免基于过期旧值直接覆盖,但竞争激烈时,反复重试会消耗资源。

ABA 指当前值经历 A 到 B 再到 A。只比较值的 CAS 可能成功,却没有发现中间发生过变化。如果业务或数据结构依赖这段变化历史,就需要把版本或标记一起纳入原子比较。

Java 中可以使用 AtomicStampedReference 等工具维护引用与版本。但是版本必须在每次相关变化时正确推进,也要考虑回绕;CAS 成功仍然不等于整个多字段业务操作都已经原子化。

CAS 看当前,版本记变化

图左侧演示把值和版本一起核对的方案;普通的单值 CAS 不会自动提供版本字段。

知识点详解:先解决覆盖更新,再看被遗漏的历史

为什么普通的“先看再改”会丢更新?

假设一个计数当前为 10,两个线程都读到 10,各自计算 11,然后分别写回。

最后只有 11,而不是希望得到的 12。问题不在加一这个算式,而在读取和写回之间,旧前提可能已经变了。

如果这两次写回都通过 CAS 比较当前值是否仍为 10,那么第一个成功更新为 11 后,第二个旧预期就不再满足。第二个必须重新读取,计算 12,再尝试更新。

这里的一次 CAS 必须由实际提供原子语义的操作支持。Java atomic 工具包提供相应单变量工具,不能用普通 if 语句模拟后就宣称线程安全。

重试时,需要重新计算,而不是重复旧答案

第二个线程第一次失败后,不能一直拿“预期 10、目标 11”去重试,然后希望容器自动理解自己的意图。

它需要重新读取当前状态,再从新状态计算新值。某些高级原子更新方法会代为重复执行更新函数,因此这个函数也应避免产生不能重复的外部副作用。

例如把“加计数”和“发送付款通知”写进可能重复执行的更新计算中,就可能让一次失败重试变成多次通知。状态更新可以重试,不代表所有伴随动作都能随便重做。

第二个线程,不能重复旧答案

ABA:值恢复了,历史却没有恢复

假设一个任务状态最初是 A。线程 1 读取 A 后暂时停下。

线程 2 把它变为 B,处理了一次任务,又把状态改回 A。线程 1 恢复后,只用值 A 比较,可能认为原来的前提仍然成立。

从“当前值”来看,它没有看错;从“从未发生过处理”的业务假设来看,它漏掉了信息。

因此,ABA 不是每次都必然造成错误。如果算法只关心当下数值,恢复为 A 可能完全可以接受。只有当旧引用、节点关系或业务状态历史影响操作正确性时,它才需要额外处理。

为什么加版本号能发现这段变化?

把比较对象从 A,改为组合状态“值 A,版本 1”。

A 变为 B 时版本推进到 2,B 再变回 A 时版本推进到 3。线程 1 再拿“值 A,版本 1”比较,虽然值相同,版本不同,更新仍然会失败。

关键不只是多放一个整数,而是:值与版本要作为同一份状态读取和比较更新,相关变化必须推进版本。

AtomicStampedReference提供引用与整数 stamp 的原子更新。使用时还要取得一致的组合状态,不要先读引用、隔一段时间再读 stamp,拼出一个从未真实存在的组合。

版本也不是永不重复的魔法。版本回绕、有人绕过规则修改、比较范围漏字段,都可能让设计失效。需要按任务生命周期、变化频率和具体实现考虑这些边界。

A 回来了,版本没回去

不加互斥锁,不代表不用协调

CAS 的原子性有底层实现和内存语义支持。它不是“完全不处理竞争”。

竞争较小时,避免阻塞可能有价值;竞争激烈时,许多线程反复失败,会增加 CPU 消耗和尾延迟。无锁算法也不等于保证每个线程都很快完成,更不等于所有业务都应该用自旋。

同时,两个独立原子变量,不会自动让“扣 A 加 B”成为不可分割的一次转账。需要保护跨字段约束时,应考虑一个统一状态、锁、事务或经过验证的并发算法。

本题是 Java 原子工具与通用并发原理,不提供普通 TS/Python 赋值伪装出来的 CAS。没有真实原子原语的教学模型,只能演示状态变化,不能证明并发安全。

面试官继续追问

CAS 失败,能直接当成业务失败吗?

要看语义。计数可以重新读取再尝试;“只在这个版本上修改”的用户操作,可能应该返回冲突,让用户确认,而不是悄悄改到新版本。

加 stamp 就能解决所有无锁结构问题吗?

不能。它帮助识别相关变化,不替代安全的节点访问、内存回收和完整算法证明。

AtomicInteger 能保护整个对象吗?

它保护相关原子操作,不会顺便让周围所有字段的组合逻辑都原子化。回答时要明确这一次比较更新包含哪些状态。

面试速记卡

  • CAS:比较与更新是一次原子操作,普通 if 加赋值不是。
  • 失败重试:读取新状态,重新计算;计算不要夹带不能重复的副作用。
  • ABA:值变回来了,不说明中间没有变化。
  • 版本方案:值与版本一起核对,相关修改要推进版本。
  • 工程边界:单次原子更新不等于跨字段事务,无锁也有竞争成本。

公司面试真题

真题根据求职者公开面经整理,题意经过概括,非逐字原话或公司官方题库;本文为 Sunday 的独立解析。

  • 美团 · 开发(含 AI 项目追问) · 原帖未明确批次

    CAS 怎样实现原子更新?(题意整理)

    美团面经 ↗
    原帖发布于 2025-09-09

  • 美团 · Java后端 · 实习

    CAS 的底层原理是什么?(题意整理)

    4.21美团Java实习一二面面经 ↗
    面试记录为 2020-04-21、2020-04-24;原帖编辑于 2020-11-14

浏览公司面试真题 →
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历