乐观并发:先改,提交时再对暗号
锁的思路是悲观——「一定有人跟我抢,先锁上再说」。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 怎么用「分摊热点」绝地翻盘。
评论 (0)