第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 并发编程不再是零散的知识点,而是一张互相连接的网。掌握这张网,面试中对任何并发问题都能追本溯源。

相关推荐
rleS IONS1 小时前
SpringBoot中自定义Starter
java·spring boot·后端
苍煜2 小时前
慢SQL优化实战教学
java·数据库·sql
AI进化营-智能译站2 小时前
ROS2 C++开发系列16-智能指针管理传感器句柄|告别ROS2节点内存泄漏与野指针
java·c++·算法·ai
TeDi TIVE3 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
二哈赛车手3 小时前
新人笔记---ES和kibana启动问题以及一些常用的linux的错误排查方法,以及ES,数据库泄密解决方案[超详细]
java·linux·数据库·spring boot·笔记·elasticsearch
嵌入式×边缘AI:打怪升级日志3 小时前
嵌入式Linux开发核心自测题(全系列精华浓缩)
java·linux·运维
FQNmxDG4S3 小时前
JVM内存模型详解:堆、栈、方法区与垃圾回收
java·jvm·算法
jason.zeng@15022074 小时前
Androidr入门环境搭建
java·kotlin
摇滚侠4 小时前
整洁的桌面和任务栏 Java 开发工程师提效方法
java·开发语言