第043篇 CAS 与原子类:无锁编程的第一课

简介: CAS是Compare-And-Swap的缩写,一种硬件级原子指令,实现“比较并交换”的乐观并发控制。它通过volatile保证可见性、CAS保证原子性,但存在ABA问题(用AtomicStampedReference加版本号解决)、高竞争下自旋耗CPU(LongAdder分段累加优化)及仅支持单变量更新等局限。

CAS 这题的分水岭在于:背过"比较并交换"的人很多,能把 ABA 问题讲清楚、知道 AtomicStampedReference 怎么解决它、能说准 LongAdder 相比 AtomicLong 凭什么在写密集场景更快的人很少。这三层恰好是面试官往下挖的方向,答到哪一层基本就定了档位。

先把结论放在前面:CAS 的全称是 Compare-And-Swap,一条原子指令,语义是"如果内存位置 V 的值仍然等于期望值 A,就把它改成 B,并返回成功;否则只返回失败、不做任何修改"。它是一条自旋重试的原子操作——失败不是阻塞,而是重新读、重新算、重新比。 原子类正是基于它实现:AtomicInteger 内部就是一个 volatile int value 加一组 compareAndSet 方法,靠 volatile 保证可见性、靠 CAS 保证原子性。

机制拆解

讲清为什么不能直接用锁。锁的思路是"互斥访问",代价是线程阻塞与上下文切换;CAS 的思路是"乐观并发"——先假设没人改,干完再检查有没有被改,有就重算。两者各有适用面:临界区短、竞争低时 CAS 明显更快;临界区长、竞争激烈时自旋会烧 CPU 反而更慢,此时锁更合适。 另外 CAS 天然只适用于单变量更新——i++ 可以拆成"读—改—写"再对写操作做 CAS,但 i 与 j 要一起改时,中间状态无法被一次 CAS 覆盖,这就是复合操作的困境。

这些坑的正确绕法

最常见的坑是 CAS 自旋在高竞争下大量空转,CPU 烧满吞吐反降。 CAS 失败后立刻重试,如果锁竞争激烈,线程会陷入"读—比—失败—再读"的循环,CPU 全耗在内存读写上。识别特征是 CPU 使用率异常高但吞吐量上不去,trace 里线程状态为 RUNNABLE 却无实际业务进展。 解决方向有两个:降低竞争(把共享变量分片,比如用 LongAdder 替代 AtomicLong),或改用锁(在临界区较长时)。

其次是拿 AtomicReference 做多字段一致性更新,忘了 ABA 或拆分更新导致状态撕裂。 两个问题要分开看。ABA 是指值经历了"A→B→A"的往返,CAS 只看值不看历史,于是误认为没变;对基本类型影响有限(值确实一样),但对引用类型会出错(对象内部可能已改变)。解法是加版本戳,即 AtomicStampedReference 维护一个引用加一个整型版本。 多字段更新则是另一回事:把对象整体放进 AtomicReference 并替换整个对象(不可变对象 + 整体替换)才是正确做法,逐字段 set 无法保证一致性。

还有一个更隐蔽的坑:把原子类当成"所有并发问题"的解法。 原子类只保证单变量的单次读写原子,AtomicInteger 里的 incrementAndGet 是原子的,但"取出值 → 判断 → 写回"这段仍然是复合操作,多线程下照样出错。判断标准很简单:如果逻辑里有"检查后动作",就需要锁;如果只是"读出来加一写回去",原子类就够。

给 CAS 与原子类建一份边界清单,注释里写清楚,单测覆盖边界与异常路径。 落地成几条:单个计数用原子类,多字段一致用不可变对象整体替换或加锁;写密集场景优先 LongAdder,读密集场景 AtomicLong 更省内存;重试型 CAS 循环要有退出条件,不做无界自旋。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// CAS 与原子类
AtomicInteger ai = new AtomicInteger();       // volatile int + CAS
ai.incrementAndGet();                          // 自旋 CAS 实现的原子自增

// 自旋重试模板:只能用于单变量的读-改-写
int prev, next;
do {
   
    prev = ai.get();
    next = prev + 1;
} while (!ai.compareAndSet(prev, next));       // 失败则重算重试

// 写密集用 LongAdder(空间换时间,Cell 分散热点)
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum();                        // 读时汇总

这段代码值得盯两处:第一处,自旋重试模板只适用于单变量,遇到"要同时看两个变量"的逻辑就必须改用锁;第二处,LongAdder 的思路是分散热点——每个线程往自己的 Cell 里加,读时再汇总,牺牲读的即时性换取写的吞吐。面试讲到这一层,基本就稳了。

这题在面试里怎么问、怎么答

"请简单介绍一下 CAS 与原子类,它在 Android 开发中起什么作用?"一句话定位:CAS 是硬件级的原子指令,原子类基于它实现无锁并发更新。再补一个真实场景——统计点击次数、给消息加序号、维护轻量的运行期开关状态。落一个细节:AtomicInteger 与 synchronized 的取舍看临界区长度与竞争强度。

"CAS 与原子类的底层原理是什么?能不能详细说一下?"从 CAS 语义讲起:一条读-比较-写指令,中途不会被其他线程打断。再讲三个经典问题:ABA(用 AtomicStampedReference 加版本号解决)、自旋开销(高竞争下改用锁或分片)、只适用单变量(复合操作必须用锁)。最后补 LongAdder 的 Cell 机制,说明它是"用空间换时间、把单点热点打散"的典型。 讲完这四块,这题基本可以给高分。

"在使用 CAS 与原子类时遇到过什么问题?"按现场、排查、根因、预防四段讲。一个典型案例:某个全局计数器在多线程写入时 CPU 使用率异常升高,吞吐却没有提升;定位到是高竞争下 CAS 自旋空转;修复为改成 LongAdder 并减少无谓的读-改-写;验证是 CPU 明显下降、吞吐反而上升。

"和相关的替代方案相比,有什么优劣?"对比 synchronized、ReentrantLock、无锁结构三条线。结论落在场景:单变量低竞争用原子类(无阻塞、无上下文切换);临界区长或竞争激烈用锁;读多写少用 AtomicLong,写密集用 LongAdder。选型时把"自旋空转烧 CPU"这类代价摆到台面上,再决定是否引入。

再补一个工程上值得讲清的点:为什么 LongAdder 要在读的时候做汇总,这正是它的取舍。AtomicLong 的每一次读都要读同一个热点变量,读写都压在同一个缓存行上,高并发下会触发缓存行的争抢。 LongAdder 的做法是维护一个 Cell 数组,每个线程(或 CPU 核)写的时候只动自己那个 Cell,读的时候把所有 Cell 加起来——写被分散了,读的成本上升但读通常不频繁,整体吞吐因此改善。 这个设计还有两个细节:Cell 数组在竞争激烈时会自动扩容(按线程探针哈希映射到不同槽),而 sum() 不是快照,中途可能有线程在写,所以它只适合统计类场景、不适合做精确的同步判断。面试里能讲出"分散热点 + 读时汇总 + 非快照读"这三点,说明不是只背过 API。

再补一个容易被问到的高频陷阱:AtomicInteger 的 getAndIncrement 与 incrementAndGet 差别在哪。前者返回自增前的旧值,后者返回自增后的新值。业务含义完全不同——做"先取值再累加剩余额度"要用前者,做"发放一张并拿到新余额"要用后者。写错一个方法名,在并发下就可能超发或少发,而这种错误在低并发测试里往往复现不了。 面试里被追问"这两个方法有什么区别"时,能答清返回值语义并给出业务例子,说明是写过的而不是背的。

给正在准备面试的你

把 CAS 画成一张流程图:读当前值 → 与期望值比对 → 相等则写入并返回成功 → 不等则回到第一步重来,并在旁边标出 ABA 与自旋两个问题的位置。面试中关于 CAS 的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。

再补工程案例与踩坑——应用落点是把项目里所有 AtomicLong 换成 LongAdder 做对照压测,分别在低并发、高并发、读多写少三种场景下记录吞吐与延迟差异。

复习时别孤立刷题:volatile——原子类靠 volatile 保证可见性、靠 CAS 保证原子性,两者分工互补。

划两句重点:CAS 失败是自旋重试而非阻塞,高竞争下会烧 CPU;ABA 用版本戳解决,多字段一致用不可变对象整体替换,"检查后动作"必须用锁。

下一篇聊 ThreadLocal 原理:主线程到底能不能用——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:CountDownLatch-与-CyclicBarrier:等待与协作

下一篇预告:ThreadLocal-原理:主线程到底能不能用

有任何问题欢迎在评论区留言交流。

相关文章
|
17天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8478 24
|
16天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2817 14
|
15天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2021 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
14天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
10天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
4天前
|
人工智能 JSON Linux
【全网最详细】ComfyUI使用教程:下载+本地部署+配置+工作流搭建一篇搞定(2026最新版)
ComfyUI是一款免费开源的本地AI绘图工具,采用节点式工作流设计,支持文生图、图生图、局部重绘、放大、换脸等多种功能。可离线运行,依赖显卡加速,无需联网。支持自定义流程保存与分享,插件生态丰富,适合进阶用户。(239字)
|
10天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)

热门文章

最新文章