← 返回题目列表

Java 的原子类有哪些?AtomicInteger 怎么实现线程安全?如何解决 ABA 问题?

高频 中等 第 11 / 31 题 更新于 2026/07/26
原子类AtomicIntegerCASAtomicStampedReference

简化版

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、多变量还得靠锁」。