├── 三、CAS 与 Atomic
│ ├── CAS 原理
比较内存中的值是否还是自己读取时的值,如果是,就修改;否则失败,再重新尝试。
a(希望的值,修改的值)
建议 内存当前的值是否是希望的值
是就修改
CAS 的"比较 + 修改"并不是两步操作,而是CPU 提供的一条原子指令(如 x86 的
CMPXCHG)完成的。
| 参数 | 含义 | 举例 |
|---|---|---|
| V | 内存中的真实值 | value=5 |
| A | 期望值(Expected) | 线程读取到5 |
| B | 新值(New) | 修改成6 |
线程读取 value=5
│
▼
准备修改为6
│
▼
CPU执行CAS
│
▼
value还是5?
│
┌─┴────────┐
│ │
是 否
│ │
修改成功 修改失败
│ │
结束 重新读取
CAS 为什么线程安全
CAS 是**乐观锁(Optimistic Lock)**思想的典型实现,认为大多数情况下不会发生冲突,冲突时再重试。
CAS 底层原理
AtomicInteger.incrementAndGet();
Unsafe.compareAndSwapInt()
VarHandle.compareAndSet()
CPU 原子指令 CMPXCHG(x86)
AtomicInteger
│
▼
Unsafe / VarHandle
│
▼
JNI
│
▼
CPU CAS 指令
│
▼
修改内存
自旋
| 优点 | 缺点 |
|---|---|
| 无锁,性能高 | 高竞争时不断重试,浪费 CPU |
| 不会发生线程阻塞 | 存在 ABA 问题 |
| CPU 原子指令保证安全 | 只能保证单个共享变量的原子性 |
| 高并发效率高 | 多变量需要额外机制(如AtomicReference、锁等) |
│ ├── ABA 问题
ABA 问题是 CAS 的经典缺陷。
CAS 只判断"当前值是否等于期望值",并不知道这个值在中间是否被别人修改过。
A
↓B
↓
A
值虽然又变回了A,但实际上已经发生过变化。
CAS 看到还是A,会误以为"没人修改过",于是修改成功。
解决方式
版本号
value=A version=1
虽然值一样,
但是版本已经变化。
CAS 能发现数据被修改过。
JDK 提供:
AtomicStampedReference(值 + 版本号)AtomicMarkableReference(值 + 标记位)
其中最常用的是:
AtomicStampedReference:每次更新同时比较引用和值对应的版本号(Stamp),只要版本号变化,即使值恢复原样,CAS 也会失败。
ABA 问题是 CAS 的典型缺陷。由于 CAS 只比较当前值是否等于期望值,而不会检查该值在中间是否发生过变化,因此当数据经历 A→B→A 后,CAS 仍会认为数据未被修改。Java 通常通过AtomicStampedReference引入版本号(Stamp),将比较对象从"值"扩展为"值 + 版本号",从而避免 ABA 问题。
超级多类
│ ├── AtomicReference
│ ├── AtomicStampedReference
│ ├── AtomicMarkableReference
│ ├── AtomicIntegerFieldUpdater
│ └── Unsafe 类
| 类 | 操作对象 | 主要作用 | 解决什么问题 | 是否解决 ABA | 典型场景 |
|---|---|---|---|---|---|
AtomicReference<T> | 引用对象 | 原子更新对象引用 | 普通 CAS 只能更新基本类型,不能方便地原子更新对象引用 | ❌ | 无锁链表、无锁队列、缓存对象切换 |
AtomicStampedReference<T> | 引用 + 版本号 | 在 CAS 的基础上增加版本号(Stamp) | 解决 ABA 问题 | ✅ | 链表、栈、队列等需要检测引用是否被修改过 |
AtomicMarkableReference<T> | 引用 + 标记位 | 给对象增加一个布尔标记(true/false) | 标记对象状态,而不是记录修改次数 | ⚠️ 部分避免(不能完全解决) | 逻辑删除、是否已处理、是否失效 |
AtomicIntegerFieldUpdater<T> | 对象中的int字段 | 不创建AtomicInteger,直接原子更新对象字段 | 减少内存占用 | ❌ | 大量对象中的计数器、状态字段 |
Unsafe | 任意内存 | 提供底层 CAS、内存操作、对象创建等能力 | JVM 底层实现并发工具 | 取决于使用方式 | JUC、Netty、Spring、JVM 底层 |