第8篇:原子类与CAS底层原理——无锁并发的实现

从一个计数器说起

java 复制代码
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // 线程不安全
    }
}

多线程下 count++ 不是原子操作,导致计数偏少。你可能会说:加 synchronized 就好了。但有没有不加锁也能保证线程安全的方式?

有------原子类AtomicIntegerincrementAndGet() 可以在不加 synchronized 的情况下实现线程安全的自增。它背后的核心就是 CAS(Compare And Swap)

面试中,CAS 是必考点。但很多人只知道 CAS 是乐观锁,却回答不上来:

  • CAS 的底层 CPU 指令是什么?
  • ABA 问题是怎么产生的,如何解决?
  • LongAdder 为什么比 AtomicLong 更快?
  • Unsafe 类为什么叫 Unsafe?

本文从底层硬件到上层 API,彻底拆解 Java 原子类和 CAS 的实现原理。

本文核心问题:

  1. CAS 是什么?它为什么能不加锁实现原子操作?
  2. CAS 的底层 CPU 指令是什么?Unsafe 类扮演了什么角色?
  3. ABA 问题是怎么产生的?如何用版本号解决?
  4. AtomicInteger 的 incrementAndGet() 源码是怎么写的?自旋重试多少次?
  5. LongAdder 为什么比 AtomicLong 在高并发下更快?Cell 是怎么分散压力的?
  6. AtomicReference、AtomicStampedReference、AtomicMarkableReference 各自适用什么场景?
  7. CAS 有什么缺点?什么情况下不适合用 CAS?
  8. 原子类在 JDK 中有哪些应用?线程池、AQS、ConcurrentHashMap 中分别怎么用?

读完本文你将对 CAS 和原子类有从硬件指令到框架应用的完整理解。


一、CAS 是什么?------一条 CPU 指令的魔法

疑问:为什么 count++ 不能保证原子性,而 AtomicInteger.incrementAndGet() 可以?

回答:因为 count++ 在字节码层面是三步操作,而 incrementAndGet() 底层用了 CAS------一条 CPU 指令完成"比较并交换"。

1.1 count++ 的实际执行过程

java 复制代码
count++;  // 一行代码

编译成字节码后,实际上是三条指令:

复制代码
1. getfield  count   // 从主内存读取 count 到操作数栈
2. iconst_1          // 常量1压入栈
3. iadd              // 加法运算
4. putfield  count   // 写回主内存

多线程下的问题:线程 A 执行到第 3 步时,线程 B 可能已经写回了新值,线程 A 再写回就会覆盖线程 B 的结果。这就是经典的"读-改-写"竞态条件。

1.2 CAS 的思想

CAS(Compare And Swap):先比较,如果当前值等于我预期的值,就交换为新值;否则不交换。

复制代码
CAS(内存地址 V, 预期值 A, 新值 B):
    if (V.value == A) {
        V.value = B;
        return true;
    } else {
        return false;
    }

关键 :这个"比较+交换"的过程是原子的,由 CPU 指令保证。

1.3 CAS 的底层实现------Unsafe 类 + CPU 指令

Java 中 CAS 操作的入口是 sun.misc.Unsafe 类:

java 复制代码
public final class Unsafe {
    // native 方法,直接调用 CPU 指令
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
}

底层 CPU 指令

  • x86/x64 架构:lock cmpxchg(Compare and Exchange)
  • ARM 架构:ldrex/strex(Load Exclusive / Store Exclusive)

lock 前缀的作用 :锁定总线或缓存行,保证多核 CPU 之间的原子性和可见性。执行 lock cmpxchg 时,其他 CPU 核心不能访问该内存地址。


二、ABA 问题------CAS 的阿喀琉斯之踵

疑问:CAS 看起来很完美,它有什么缺陷?什么是 ABA 问题?

回答:CAS 只比较值,不关心值的变化过程。如果一个值从 A 变成 B 又变回 A,CAS 发现还是 A,就会认为没有变化------这就是 ABA 问题。

2.1 ABA 问题的场景

复制代码
初始值: A

线程 T1: 读取值为 A,准备 CAS 设为 C
线程 T2: 读取值为 A,CAS 设为 B(成功)
线程 T3: 读取值为 B,CAS 设回 A(成功)
线程 T1: CAS(预期A → 新值C) 成功! ← 但实际值已经经历过 A→B→A 的变化

单链表中的后果

复制代码
原链表: head → A → B → C → null

线程 T1: 准备 CAS 删除节点 A(将 head 指向 B)
线程 T2: 
  1. 删除 A(head 指向 B)
  2. 删除 B(head 指向 C)
  3. 重新插入 A(head 指向 A → C → null)
线程 T1: CAS 成功,head 指向 B
结果: head → B,但 B 已经被删除了!链表断裂!

2.2 解决方案:版本号

核心思想:除了比较值,还比较版本号。每次修改,版本号递增。

Java 提供了两个带版本号的原子类:

版本号机制 适用场景
AtomicStampedReference 整型版本号(stamp) 需要精确跟踪修改次数
AtomicMarkableReference 布尔标记位 只关心"是否被修改过"
java 复制代码
// AtomicStampedReference 示例
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stamp = new int[1];
String value = ref.get(stamp);  // value = "A", stamp = 0

// 线程 T1
int oldStamp = stamp[0];
ref.compareAndSet("A", "C", oldStamp, oldStamp + 1);  // CAS 同时检查值和版本号

// 线程 T2: A → B → A  (版本号 ++)
ref.compareAndSet("A", "B", 0, 1);
ref.compareAndSet("B", "A", 1, 2);

// 线程 T1 重试时:预期版本号=0,实际版本号=2 → CAS 失败

三、AtomicInteger 源码精读------自旋+CAS 的模板

疑问:AtomicInteger.incrementAndGet() 里面做了什么?如果没有成功怎么办?

回答:核心就是一个 do-while 循环------不断尝试 CAS,直到成功。这叫"自旋"。

3.1 incrementAndGet() 源码

java 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;  // value 字段的内存偏移量
    private volatile int value;             // 实际值,volatile 保证可见性
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(
                AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

// Unsafe.getAndAddInt()
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);      // 1. 获取当前值(volatile读)
    } while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. CAS尝试更新,失败则重试
    return v;
}

自旋流程图

复制代码
getAndAddInt(obj, offset, 1):
   │
   └── 循环开始
        │
        ├── 读取当前 value(假设为 5)
        │
        ├── CAS(预期5 → 新值6)
        │      │
        │      ├── 成功 → 返回 5(旧值)
        │      │
        │      └── 失败(期间其他线程修改了 value → 例如变成了6)
        │              │
        │              └── 重新循环:读取新值6 → CAS(预期6 → 新值7)
        │                     │
        │                     └── ...

3.2 自旋的优缺点

优点

  • 不需要线程挂起/唤醒(没有内核态切换)
  • 在竞争小时效率极高,只比普通赋值多一次 CAS 的开销

缺点

  • 竞争激烈时,大量线程不断自旋,CPU 空转
  • 单个共享变量的 CAS 修改会成为瓶颈(所有线程在同一个缓存行上竞争)

什么时候不适合 CAS?

  • 竞争非常激烈的场景 → 改用 LongAdder(分散压力)
  • 操作复杂(多条指令的复合操作)→ 用 synchronized 或 ReentrantLock
  • 需要阻塞等待的场景 → 用 Condition/LockSupport

四、LongAdder 为什么比 AtomicLong 更快?

疑问:高并发下 AtomicLong 性能急剧下降,LongAdder 是怎么解决的?

回答:LongAdder 的核心思想是"分散压力"------将单一计数器拆成多个 Cell,不同线程修改不同的 Cell,最终求和。

4.1 Striped64 的架构

LongAdder 继承自 Striped64

java 复制代码
abstract class Striped64 extends Number {
    static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
    }
    
    transient volatile Cell[] cells;          // 多个 Cell 的数组
    transient volatile long base;             // 基础值
    transient volatile int cellsBusy;         // 扩容锁(CAS 标志)
}

4.2 add() 的执行逻辑

java 复制代码
// LongAdder.add(long x)
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 第一次尝试:CAS 直接更新 base
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        // 找到当前线程对应的 Cell
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);  // Cell 初始化/扩容/兜底
    }
}

三步策略

  1. 尝试 CAS 更新 base:如果没有 Cell 数组,直接 CAS 修改 base,失败再进入第二步
  2. 定位 Cell:通过 ThreadLocalRandom 的探针值(probe)哈希到某个 Cell,尝试 CAS 更新该 Cell
  3. 兜底处理 :如果步骤 2 的 Cell 为空或 CAS 失败,进入 longAccumulate:创建新 Cell、扩容数组、或 CAS 重试

4.3 性能对比

复制代码
场景:10 个线程各执行 1 千万次 increment()

AtomicLong:  
  - 所有线程竞争同一个 volatile 变量
  - CAS 重试次数多
  - 耗时: ~800ms

LongAdder:
  - 线程分散到多个 Cell 上
  - CAS 几乎不碰撞
  - 耗时: ~200ms (快 4 倍)

4.4 LongAdder 的短板

sum() 是弱一致性的:求和时并发的修改可能不被计入。

java 复制代码
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;  // 遍历累加时,其他线程可能正在修改
        }
    }
    return sum;  // 近似值!
}

适用场景:统计 QPS、计数器、累加器等频繁写、偶尔读的场景。如果需要强一致性的精确值,还是用 AtomicLong。


五、其他原子类一览

5.1 AtomicBoolean / AtomicInteger / AtomicLong

基本类型的原子操作,支持 getAndIncrementcompareAndSet 等常用方法。

5.2 AtomicReference

对普通对象的原子更新(CAS 比较的是引用地址)。

java 复制代码
AtomicReference<User> ref = new AtomicReference<>(new User("张三"));
ref.getAndSet(new User("李四"));  // 原子替换
ref.compareAndSet(oldUser, newUser);  // CAS 替换

5.3 AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray

数组元素的原子更新:

java 复制代码
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.incrementAndGet(5);  // 第 5 个元素原子自增

5.4 AtomicIntegerFieldUpdater

对已有类的某个 volatile int 字段做原子更新,无需替换整个变量为 AtomicInteger:

java 复制代码
class User {
    volatile int score;  // 必须 volatile
}

AtomicIntegerFieldUpdater<User> updater = 
    AtomicIntegerFieldUpdater.newUpdater(User.class, "score");
updater.incrementAndGet(user);  // user.score 原子自增

优势:节省内存(不用为每个字段创建 AtomicInteger 对象)。


六、原子类在 JDK 中的应用

组件 使用的原子类/CAS 用途
AQS Unsafe.compareAndSwapInt 修改 state 状态
ConcurrentHashMap Unsafe.compareAndSwapObject 插入新节点、迁移数据
ThreadPoolExecutor AtomicInteger ctl 高 3 位存运行状态,低 29 位存线程数
LinkedBlockingQueue AtomicInteger count 原子计数,支持双锁并发
LongAdder / Striped64 CAS + Cell[] 高并发计数器
CopyOnWriteArrayList ReentrantLock + 数组复制 写时复制(CAS 不适用于数组修改)

七、常见面试追问

Q1:CAS 是怎么保证可见性的?

CAS 底层 lock cmpxchg 指令会锁定缓存行或总线,执行期间在总线上发送信号,使其他 CPU 核心的对应缓存行失效(写回并 invalidate)。CAS 成功后,新值对其他核心立即可见。volatile 的读写也是通过类似机制保证可见性的。

Q2:CAS 可以完全替代 synchronized 吗?

不能。CAS 适用于单变量的原子更新。需要保护多行代码、操作多个变量、或进行复杂判断时,还是要用 synchronized。轻量级锁在竞争不激烈时和 CAS 性能相差不大,竞争激烈时重量级锁的阻塞反而能防止 CPU 空转。

Q3:为什么 AtomicIntegerint 作为字段,而不是 long

它提供了 AtomicLong 处理 64 位。如果是 32 位 CPU 上,64 位变量的读写本身不是原子的(需要两个指令),只有 volatile 的 64 位变量才保证原子性。AtomicLong 在 32 位 CPU 上使用 CAS 比较 64 位,可能受限于硬件。

Q4:Unsafe 类的 compareAndSwapIntgetAndAddInt 有什么关系?

compareAndSwapInt 是 CAS 原子指令的直接封装。getAndAddInt 是更上层的封装------内部调用 compareAndSwapInt 做自旋。AtomicInteger.incrementAndGet()Unsafe.getAndAddInt(this, valueOffset, 1) + 1 → 循环调用 compareAndSwapInt


总结

复制代码
                      原子类全景图

┌──────────────────────────────────────────────────────────────┐
│                      底层:CPU 指令                           │
│         x86: lock cmpxchg    ARM: ldrex/strex               │
└──────────────────────┬───────────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────────┐
│                   JVM 层:Unsafe 类                           │
│  compareAndSwapInt(Object o, long offset, int expect, int x) │
└──────────────────────┬───────────────────────────────────────┘
                       │
           ┌───────────┼───────────┬──────────────┐
           ▼           ▼           ▼              ▼
┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│ AtomicInteger│ │AtomicLong│ │AtomicRef │ │  LongAdder   │
│ 自旋+CAS     │ │ 自旋+CAS │ │ 自旋+CAS │ │ CAS+Cell分散│
│ 单个变量     │ │ 单个变量 │ │ 单个引用 │ │ 高性能累加   │
└──────────────┘ └──────────┘ └──────────┘ └──────────────┘

核心要点回顾

  • CAS 是一条 CPU 原子指令(lock cmpxchg),实现"比较+交换"的无锁原子操作
  • Java 通过 Unsafe 类调用 CAS,上层封装为 AtomicInteger、AtomicReference 等原子类
  • ABA 问题:值从 A 变 B 又变回 A,CAS 无法感知。解决方案:带版本号(AtomicStampedReference)
  • 原子类自旋 CAS:do-while 循环重试,竞争小性能极高,竞争大会导致 CPU 空转
  • LongAdder 通过 base + Cell[] 分散 CAS 压力,高并发性能显著优于 AtomicLong,但 sum() 是弱一致性
  • CAS 适用于单变量原子更新,复杂操作仍需 synchronized 或 Lock
  • 原子类在 AQS、ConcurrentHashMap、线程池等框架中被广泛使用

并发系列总结:从第1篇 JMM 与 volatile,到第8篇原子类与 CAS,我们完成了从内存模型到锁机制,从线程管理到并发容器的完整知识体系。Java 并发编程不再是零散的知识点,而是一张互相连接的网。掌握这张网,面试中对任何并发问题都能追本溯源。

相关推荐
彦为君12 小时前
Spring定时任务开发指南(动态实现)
java·开发语言·后端·python·spring·wpf
英辰朗迪AI获客12 小时前
Claude 官方插件生态落地应用指南
java·linux·运维
今天背单词了吗98012 小时前
缓存与数据库双写不一致问题及终极解决方案(高频面试题)
java·数据库·学习·缓存
SimonKing12 小时前
裁员、降薪潮来了,你被波及了么?
java·后端·程序员
装不满的克莱因瓶13 小时前
新版AI开发框架SpringAIAlibaba vs AgentScope 选型指南
java·开发语言·人工智能·ai·agent·alibaba·springai
凯瑟琳.奥古斯特13 小时前
原码与补码乘法符号位处理差异
java·开发语言·职场和发展
iiiiyu13 小时前
面向对象案例
java·大数据·开发语言·数据结构·python·编程语言
yanghuashuiyue13 小时前
关于Eclipse和IDEA对比
java·ide·intellij-idea
Nontee13 小时前
三大范式是什么?
java·前端·数据库
pursuit_csdn13 小时前
力扣周赛 503
java·算法·leetcode