CAS不是锁,而是依赖CPU原子指令(如cmpxchg)实现的无锁并发控制,需配合volatile保证可见性,失败时通过重试而非阻塞来处理竞争,适用于低冲突场景。

为什么 CAS 不是“锁”却能保证线程安全
CAS 本质是一条 CPU 原子指令(比如 x86 上的 cmpxchg),不是 Java 层面的锁机制,也不进内核态。它靠硬件保证“比较+写入”这一步不可拆分——哪怕多个线程同时执行 compareAndSwapInt,也只会有一个成功,其余失败后立刻返回,不挂起、不阻塞、不切换上下文。
关键在于:它必须搭配 volatile 字段使用。否则线程可能一直读到自己工作内存里的旧值,导致预期值 E 永远不变,CAS 永远失败或永远“误成功”。volatile 强制每次读都从主内存拉最新值,写完立刻刷回主内存,这是 CAS 能工作的前提。
- 没加
volatile的字段上用AtomicInteger?行为未定义,大概率出错 - CAS 失败不等于出错,而是告诉你“值被别人改了”,得你自己决定重试还是放弃
- 高竞争下自旋开销明显,CPU 白跑循环;线程数远超 CPU 核心数时,性能反而不如
synchronized
AtomicInteger.getAndIncrement() 底层到底在干什么
调用 getAndIncrement() 时,JVM 实际执行的是 Unsafe.getAndAddInt():先用 getIntVolatile() 读当前值 V,再用 compareAndSwapInt(obj, offset, V, V + 1) 尝试更新。失败就重试,直到成功为止。
这个过程没有锁对象、没有 monitor entry、不触发线程调度——但它是“乐观”的:假设冲突少,靠重试兜底。所以适合计数器、状态标志位这类低竞争、单变量、操作快的场景。
- 不要在循环里反复调用
getAndIncrement()做大量累加——每次都是独立 CAS,失败重试成本叠加 - 想批量加 100?别写 100 次
incrementAndGet(),用addAndGet(100)一次到位 -
lazySet()和set()都写值,但前者不保证后续读的可见顺序,仅适用于“只写不读”的发布场景(如链表尾指针更新)
ABA 问题不是理论陷阱,是真实会崩掉逻辑的 Bug
线程 A 读到值 A,准备 CAS 成 B;此时线程 B 把 A → B → A 改了两轮;A 再执行 CAS 时发现还是 A,就直接写入——但语义已错:中间状态丢失了。
典型例子是并发栈的 pop 操作:头节点被弹出又压回同个对象,CAS 认为“没变”,实际栈结构已被破坏。
- 用
AtomicStampedReference加版本号,每次修改 stamp +1,CAS 变成“值+版本”双校验 - 用
AtomicMarkableReference加一个布尔标记位,适合只需要区分“是否被改过”的轻量场景 - 别自己用
long拼戳记——高位存版本、低位存引用,容易因 GC 移动对象地址而失效(Java 9+ 后Unsafe.objectFieldOffset对压缩指针有兼容性风险)
Unsafe 类为什么“危险”,又为什么绕不开
sun.misc.Unsafe 是 JUC 原子类的底层引擎:所有 compareAndSwapXXX、putObjectVolatile、objectFieldOffset 都依赖它。但它绕过 JVM 安全检查,可直接读写内存地址、强制实例化抽象类、甚至篡改 final 字段。
Java 9 开始模块化后,默认禁止应用代码反射访问 Unsafe;Java 17 更进一步限制其部分方法。但 JUC 内部仍通过特权块调用——你写业务代码不该碰它,但得知道 Atomic 类背后是它在扛。
- 别在应用里 try-catch
PrivilegedActionException去硬取Unsafe实例,JDK 版本一升级就挂 -
VarHandle(Java 9+)是官方推荐替代方案,API 更安全、语义更清晰,但目前Atomic类还没全面迁过去 - 看到堆栈里出现
Unsafe.compareAndSwapInt不用慌——那是正常路径;但如果自己代码里出现了,基本可以删了
真正难的从来不是“怎么调用 incrementAndGet”,而是判断该不该用它、值被谁改过、失败后要不要等、以及当多个字段要一起变时——CAS 已经帮不上忙了。










