Java 的原子类有哪些?AtomicInteger 怎么实现线程安全?如何解决 ABA 问题?
简化版
java.util.concurrent.atomic 提供一批无锁线程安全的原子类,靠 CAS(比较并交换)实现,比加锁更轻量。主要分五类:基本类型(AtomicInteger/AtomicLong/AtomicBoolean)、引用类型(AtomicReference)、解决 ABA 的带版本引用(AtomicStampedReference/AtomicMarkableReference)、数组类型(AtomicIntegerArray 等)、字段更新器(AtomicIntegerFieldUpdater)。Java 8 还加了高并发累加器 LongAdder/LongAccumulator。AtomicInteger.incrementAndGet() 用「CAS + 失败自旋重试」保证原子自增;ABA 问题(值变回原样看不出被改过)用 AtomicStampedReference(加版本号)解决。
详细版
原子类五大家族:
| 类别 | 代表类 | 用途 |
|---|---|---|
| 基本类型 | AtomicInteger、AtomicLong、AtomicBoolean | 原子的整型/布尔操作 |
| 引用类型 | AtomicReference | 原子地更新对象引用 |
| 带版本/标记引用 | AtomicStampedReference、AtomicMarkableReference | 解决 ABA 问题 |
| 数组 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray | 原子更新数组某个下标 |
| 字段更新器 | AtomicIntegerFieldUpdater 等 | 原子更新已有类的 volatile 字段 |
| 高并发累加器(Java 8) | LongAdder、LongAccumulator | 高并发计数,比 AtomicLong 快 |
AtomicInteger 的原子自增原理(CAS + 自旋):
public final int incrementAndGet() {
int prev, next;
do {
prev = get(); // 读当前值
next = prev + 1; // 算新值
} while (!compareAndSet(prev, next)); // CAS:若当前仍是 prev 才写 next,否则重试
return next;
}
// compareAndSet 底层是一条 CPU 原子指令(cmpxchg),不需要加锁
CAS 逻辑:「如果内存值仍等于我读到的 prev,就把它改成 next;否则说明被别人改了,返回失败」。失败就循环重读重试(自旋),直到成功。全程无锁。
ABA 问题与解决:
// ABA:值从 A 变成 B 又变回 A,CAS 以为"没变过",但其实中间被动过手脚
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 值+版本号
int stamp = ref.getStamp();
ref.compareAndSet(100, 101, stamp, stamp + 1); // 同时比对值和版本号
// 只有"值和版本号都匹配"才更新,版本号只增不减,A→B→A 也能被识别(版本号变了)
⚠️ 原子类只保证「单个变量」的原子性。要原子地更新多个变量或复合逻辑,原子类做不到——那需要锁,或把多个变量封装进一个对象用 AtomicReference 整体替换。
完整版教学
一、为什么要原子类:锁的轻量替代
多线程下 count++ 不安全(读-改-写三步会交错)。最直接的办法是加锁 synchronized,但锁有代价:阻塞、上下文切换、可能死锁。原子类提供了无锁的另一条路:
加锁方案: CAS 方案(原子类):
lock() do {
count++ ← 互斥 read → compute → CAS
unlock() } while(失败重试) ← 无锁,失败就重试
线程阻塞等待 线程不阻塞,一直"自旋"尝试
CAS 的思想是「乐观」:先假设没人和我竞争,直接尝试改;万一失败(说明有人抢先改了),再重试。在竞争不激烈时,CAS 几乎每次一发命中,比加锁(哪怕无竞争也要走锁的流程)更快、无阻塞风险。这就是原子类的价值——用乐观的 CAS 换掉悲观的锁。
二、CAS 是什么:一条硬件原子指令
CAS = Compare And Swap,三个参数:内存位置 V、期望值 A、新值 B。语义是「当且仅当 V 处的值等于 A,才把它改成 B,并返回是否成功」。
CAS(V, A, B):
原子地执行:
if (V == A) { V = B; return true; }
else return false;
关键在「原子地」——这不是三条 Java 语句,而是 CPU 提供的单条指令(x86 的 cmpxchg),硬件保证它执行期间不被打断。Java 通过 Unsafe 类调用它。所以 CAS 本身天然线程安全,不需要任何锁。AtomicInteger 的所有原子操作,最终都落到这条指令上。
三、incrementAndGet 的自旋:失败就重来
单条 CAS 只能「尝试一次」,而自增需要「一定成功」,所以要自旋(循环重试):
两个线程同时对 count=5 自增:
线程A: read 5 → 算 6 → CAS(5→6) 成功 → count=6
线程B: read 5 → 算 6 → CAS(5→6) 失败(此刻 count 已是6,不等于期望的5)
→ 重试:read 6 → 算 7 → CAS(6→7) 成功 → count=7
最终 count=7,两次自增都生效,无丢失
CAS 失败不代表出错,而是「有人抢先了,我重读最新值再试」。这个「读-算-CAS-失败重试」的循环就是自旋。代价:高并发下大量线程同时 CAS 同一个变量,会有很多次失败重试,CPU 空转严重(自旋开销)。这也引出了 LongAdder 的必要性。
四、ABA 问题:值一样不代表没被动过
CAS 只看「值等不等于期望值」,这带来一个隐患——ABA:
线程1 读到 count = A(100),准备 CAS(A→C)
期间:线程2 把 100 改成 200(B),又改回 100(A)
线程1 的 CAS(100→C):发现值还是 100,成功!
→ 但它不知道中间已经历 A→B→A,值"变回来"了
多数情况 ABA 无害(值一样,逻辑上等价)。但在依赖「中间状态」的场景会出 bug,典型是无锁栈/无锁链表:节点被弹出又压回,指针值相同但对象已不同,CAS 误判导致链表结构错乱。解决办法是给值加一个只增不减的版本号——不光比对值,还比对版本,AtomicStampedReference 就是干这个的。A→B→A 后版本号已从 0 变成 2,CAS 一比对版本号就发现「被动过」。
五、LongAdder:高并发计数为什么比 AtomicLong 快
AtomicLong 在高并发下的痛点是「所有线程抢一个变量做 CAS」,竞争激烈时自旋失败率飙升。LongAdder 的思路是「分而治之」:
AtomicLong: 所有线程 → 竞争同一个 value → 大量 CAS 失败重试
[value] ← 热点,争抢
LongAdder: 把计数拆散到多个 Cell(槽)
线程1 → Cell[0]
线程2 → Cell[1] 各加各的,几乎不冲突
线程3 → Cell[2]
最终值 = base + Cell[0] + Cell[1] + ...(sum 时汇总)
核心是空间换时间 + 热点分散:多个线程分散到不同 Cell 上累加,把「一个热点」变成「多个冷点」,CAS 冲突大幅减少。读取时把所有 Cell 求和。代价是:sum() 不是绝对精确的实时快照(求和期间可能有 Cell 在变),且占用更多内存。所以高并发只写偶尔读的计数(如 QPS 统计、限流计数)用 LongAdder;需要精确实时值或低并发用 AtomicLong。
六、字段更新器与选型总结
AtomicIntegerFieldUpdater 等更新器能原子更新一个已有类里的 volatile 字段,而不必把字段类型改成 AtomicInteger(省去每个对象都持有一个 Atomic 实例的内存开销),适合海量对象、偶尔需要原子更新某字段的场景。选型全景:
| 需求 | 选择 |
|---|---|
| 原子整型/布尔 | AtomicInteger/AtomicLong/AtomicBoolean |
| 原子更新对象引用 | AtomicReference |
| 有 ABA 风险 | AtomicStampedReference(版本号) |
| 只关心「改没改过」不关心变几次 | AtomicMarkableReference(布尔标记) |
| 高并发计数 | LongAdder |
| 海量对象省内存的原子字段 | AtomicXxxFieldUpdater |
| 更新多个变量/复合逻辑 | 加锁 或 AtomicReference 整体替换 |
记忆钩子:「原子类 = CAS + 自旋,无锁;五大家族记基本/引用/带版本/数组/字段更新器;ABA 用 AtomicStampedReference 加版本号;高并发计数用 LongAdder 分散热点」。
七、常见误区与追问
- 误区:原子类能保证多个变量的原子性。 只保证单个变量;多变量或复合逻辑要加锁,或封装成对象用 AtomicReference 整体替换。
- 误区:CAS 一定成功。 单次 CAS 可能失败(被别人抢先),靠自旋循环重试直到成功;高并发下失败重试多,CPU 空转是其代价。
- 误区:AtomicInteger 高并发下性能一定好。 竞争激烈时大量 CAS 失败自旋,反而不如 LongAdder;LongAdder 靠分散 Cell 降低冲突。
- 误区:ABA 问题一定会导致 bug。 多数场景值相同即等价、无害;只有依赖「中间状态」的无锁数据结构(栈/链表)才需用带版本号的原子类。
- 追问:CAS 有哪三个问题? ①ABA(用版本号解决);②高并发自旋开销大(用 LongAdder 或锁);③只能保证单个变量原子(用锁或 AtomicReference 封装多变量)。
- 追问:LongAdder 的 sum() 精确吗? 不是强一致的实时快照——求和遍历各 Cell 期间可能有并发修改;适合统计类场景,需要精确实时值用 AtomicLong。
- 追问:AtomicStampedReference 和 AtomicMarkableReference 区别? 前者用 int 版本号(记录变了几次),后者用 boolean 标记(只记「有没有被改过」);需要知道具体变更次数用前者。
八、加强记忆
原子类是「无锁的线程安全」方案,底层是 CAS(比较并交换)——一条 CPU 原子指令(cmpxchg),语义「值还等于期望值才更新」,配合自旋(失败就重读重试)实现如 incrementAndGet 的原子自增。它用「乐观」替代「悲观加锁」:无竞争时比锁快、无阻塞。五大家族要记全:基本类型(AtomicInteger/Long/Boolean)、引用(AtomicReference)、带版本引用(AtomicStampedReference,解决 ABA)、数组、字段更新器(省内存),外加 Java 8 的 LongAdder(分散 Cell 降低热点冲突,高并发计数首选)。CAS 的三个短板要背熟:ABA(值变回原样看不出,用版本号解决)、自旋开销(高并发失败重试多,用 LongAdder/锁)、只能保单变量(多变量用锁或 AtomicReference 整体替换)。一句话「CAS+自旋无锁原子,五大家族全覆盖,ABA 加版本、高并发用 LongAdder、多变量还得靠锁」。