连载中 7/21

CAS:不加锁也能线程安全,前提是懂它的三个软肋

2026-10-26 · 56 阅读 · 0 评论 · 0 赞

乐观并发:先改,提交时再对暗号

锁的思路是悲观——「一定有人跟我抢,先锁上再说」。CAS 是乐观——「大概没人抢,我先改,提交时核对一眼:那个值还是我读到的吗?是就替换成功,不是就重来」。compare and swap 三个操作数:内存地址 V、期望值 E、新值 N——V 等于 E 才把 V 更新为 N,否则什么都不做并返回失败。核对加替换是一条 CPU 指令(cmpxchg),硬件级原子,不存在被人插队的中间状态。

AtomicInteger 的内核

原子类就是 CAS 的标准封装。getAndIncrement 的内核(JDK 8 源码骨架)长这样:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);          // 读最新值
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // CAS 失败就重来
    return v;
}

一个自旋循环:读最新值、CAS 替换,失败说明有人抢跑,再来一轮。没有锁、没有 BLOCKED、没有上下文切换——冲突不激烈时,CAS 比 synchronized 轻快得多。注意那个读用的也是 volatile 语义(getIntVolatile),保证每次都拿到最新值,这也是 CAS 能正常工作的前提。

软肋一:ABA 问题

CAS 只核对「值相等」,不问「中间经历了什么」。线程 1 读到 A,还没来得及 CAS;线程 2 把值改成 B,又改回 A;线程 1 的 CAS 照样成功——但对象可能早已面目全非。经典案例是无锁栈:节点 A 弹出后被回收又复用,指针转了一圈回到原位,CAS 以为一切没变,其实栈结构已经被改坏。值相等,不等于状态没变。解法是版本号:每次修改版本号加一,核对「值 + 版本」两项——AtomicStampedReference 就是这么干的;只关心「动没动过」的场合还有个简化版 AtomicMarkableReference(版本换成布尔标记)。

软肋二:自旋空耗 CPU

竞争激烈时,CAS 反复失败反复重试,线程一直空转——CPU 白烧,效率反而不如「抢不到就挂起」的锁。所以原子类的适用边界是低冲突场景;高并发统计怎么办?LongAdder 的分摊方案,下一篇见。

软肋三:一次只能管一个变量

CAS 只能对单个变量做原子替换。要原子地同时改两个值,办法是 AtomicReference:把两个变量打包成一个不可变对象,对这个引用整体 CAS——要么全换,要么不换。实在打包不动(对象太大),那就老实用锁。

CAS 的哲学是「失败就重试」,赌的是冲突少。可如果冲突真的多到重试不起呢?下一篇拆原子类全家桶,看 LongAdder 怎么用「分摊热点」绝地翻盘。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Java#CAS#AtomicInteger#ABA#JUC

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10113 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9876 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9306 阅读 · 21 评论 · 287 赞