静下心来,把 JDK 源码翻了一遍,发现 JUC 的地基其实很清晰——volatile 保证可见性,CAS 保证原子性,Unsafe 是底层操作入口。

这篇文章,把 JUC 学习的第一和第二阶段串起来讲,带源码分析。


一、先搞懂 JMM:并发问题的根源

1.1 为什么会有并发问题?

CPU 太快,内存太慢。所以 CPU 加了一层缓存:

┌─────────────────────────────────────────┐
│                 主内存                    │
│           共享变量存在这里                  │
└───────────────┬─────────────────────────┘
                │
        ┌───────▼───────┐
        │   CPU 缓存    │   ← 线程读写的是缓存,不是主内存
        │  (工作内存)   │
        └───────────────┘

问题来了:

线程 A 修改了变量 flag = true,写到了 CPU 缓存里,还没来得及写回主内存。

线程 B 此时读 flag,读的是自己缓存里的旧值,看到的还是 false。

这就是可见性问题

1.2 JMM 的抽象

JMM(Java Memory Model)是 Java 规范定义的抽象概念,屏蔽了不同硬件的内存模型差异。

它规定了两条核心规则:

  1. 可见性:一个线程对共享变量的修改,其他线程能看见
  2. 有序性:指令重排不能影响执行结果

二、volatile:JMM 的钥匙

2.1 volatile 能做什么?

volatile boolean flag = false;

// 线程 A
flag = true;

// 线程 B
while (!flag) {
    // 等待
}
System.out.println("flag 变成 true 了");

加了 volatile 之后,线程 B 一定能读到线程 A 修改后的值。

volatile 保证两点:

  1. 可见性:写完立刻刷回主内存,读完立刻从主内存加载
  2. 有序性:禁止指令重排

2.2 volatile 不保证什么?

volatile int count = 0;

// 线程 A
count++;  // 读取 → 加1 → 写回

// 线程 B
count++;  // 同时也在做读取 → 加1 → 写回

两个线程都执行了 count++,但最终结果可能是 1,不是 2。

因为 count++ 是三步操作(读-改-写),volatile 只能保证单次读/写的可见性,复合操作不保证原子性。

2.3 volatile 的底层实现:内存屏障

JVM 在 volatile 读写时会插入内存屏障

volatile boolean flag = true;  // 写

编译成汇编会多一条指令:

mov    BYTE PTR [rax+0x68], 0x1   ; 写入值
lock   addl $0x0, (%rsp)          ; LOCK 前缀 = 内存屏障

LOCK 前缀做了什么?

LOCK 前缀
  │
  ├─ 强制将 CPU 缓存的数据写回主内存
  │
  ├─ 通过 MESI 协议使其他 CPU 缓存行失效
  │   (线程 B 下次读必须从主内存重新加载)
  │
  └─ 禁止指令重排(StoreLoad 屏障)

Happens-Before 规则:对 volatile 变量的写,happens-before 后续对该变量的读。

// 线程 A
sharedData = 42;     // 普通写
flag = true;         // volatile 写

// 线程 B
while (!flag) {}     // volatile 读
System.out.println(sharedData);  // 一定输出 42

重点:sharedData 的修改对线程 B 可见,因为 volatile 的写 happens-before 规则保证了顺序。

2.4 面试高频追问:单例模式为什么要加 volatile?

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 分三步:

1. 分配内存
2. 调用构造方法初始化对象
3. 将 instance 指向这块内存

没有 volatile,步骤 2 和 3 可能重排。

线程 A 执行到步骤 3(还没执行步骤 2),线程 B 刚好进入 getInstance(),发现 instance 不为 null,直接返回了一个未初始化完成的对象

volatile 禁止了步骤 2 和 3 重排,所以必须加。


三、CAS:无锁并发的核心

3.1 CAS 是什么?

CAS(Compare And Swap,比较并交换)是 CPU 指令级别的原子操作。

CAS(内存地址, 期望值, 新值)
  → 若地址的值 == 期望值,将地址的值改成新值,返回 true
  → 否则什么都不做,返回 false

整个过程是原子的,CPU 保证。

3.2 CAS 在 x86 上的实现

LOCK CMPXCHG [地址], 新值
  • CMPXCHG:比较寄存器 EAX 和内存地址的值
  • 相等:ZF=1,把新值写入内存
  • 不等:ZF=0,把内存值加载到 EAX
  • LOCK 前缀:多核环境下锁定总线,保证原子性

3.3 CAS 的自旋模式

CAS 不阻塞线程,失败了怎么办?重来。

// getAndAddInt 底层实现
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);      // 读取当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // CAS 失败就重试
    return v;
}

死循环 + CAS,就是无锁并发的核心模式。


四、Unsafe 类:绕过 JVM 的后门

4.1 Unsafe 是什么?

sun.misc.Unsafe 提供了直接操作内存的能力,CAS 就是通过它暴露给 Java 的。

Unsafe 不能直接 new,需要反射获取:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

4.2 Unsafe 的 CAS 方法

// 三个核心 native 方法
public final native boolean compareAndSwapInt(
    Object obj,    // 目标对象
    long offset,   // 字段内存偏移量
    int expected,  // 期望值(旧值)
    int update     // 新值
);

public final native boolean compareAndSwapLong(
    Object obj, long offset, long expected, long update);

public final native boolean compareAndSwapObject(
    Object obj, long offset, Object expected, Object update);

这些方法都是 native 的,JVM 底层调用 C++,C++ 调用汇编指令。

4.3 字段偏移量:怎么知道值存在哪?

JVM 中每个对象在内存里的布局是固定的。Unsafe.objectFieldOffset() 能算出字段相对于对象头的偏移量:

private static final long valueOffset;
static {
    valueOffset = unsafe.objectFieldOffset(
        AtomicInteger.class.getDeclaredField("value"));
}

拿到偏移量,Unsafe 就能直接操作这个位置的值。


五、AtomicInteger 源码:CAS 的完整应用

5.1 核心结构

public class AtomicInteger extends Number implements java.io.Serializable {

    // Unsafe 实例,CAS 操作的入口
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    // value 字段在内存中的偏移量
    private static final long valueOffset;

    static {
        try {
            // 类加载时算出偏移量,后续操作直接用
            valueOffset = unsafe.objectFieldOffset(
                AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    // volatile 保证可见性
    private volatile int value;

    // ...
}

5.2 incrementAndGet:自增操作

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

调的还是 Unsafe 的 getAndAddInt,自旋 CAS 实现。

5.3 compareAndSet:CAS 更新

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

直接调用 Unsafe 的 CAS,语义就是"期望值等于当前值,才更新成新值"。

5.4 getAndAccumulate:自定义累加

public final int getAndAccumulate(int x, IntBinaryOperator func) {
    int prev, next;
    do {
        prev = get();                      // 读取当前值
        next = func.applyAsInt(prev, x);   // 计算新值
    } while (!compareAndSet(prev, next));  // CAS 失败重试
    return prev;
}

这里有个容易忽略的细节:自旋 CAS 在高并发下可能消耗大量 CPU。


六、ABA 问题:C最讨厌的坑

6.1 ABA 是什么?

T1 读取值 A
T2 把 A 改成 B
T2 把 B 改回 A          ← 值变回来了
T1 做 CAS(A, C),成功   ← 但中间已经变过

CAS 只比较值,不记录"中间变过"。

6.2 ABA 的危害场景

// 栈的 pop 操作
// 初始:栈顶是 A
Node top = stack.top;

// 在 T1 执行 CAS 之前,T2 做了:
// 1. pop A → top = B
// 2. push 新节点 A → top = A
// 此时值又变回 A,但 A 已经不是原来的 A 了

// T1 执行 CAS(top, A, B),成功
// 但此时 B 可能已经被 T2 pop 过,栈结构已经损坏

6.3 解决方案:加版本号

// AtomicStampedReference:值 + 版本号
AtomicStampedReference<String> ref = 
    new AtomicStampedReference<>("A", 1);

// T1:读取值和版本号
int[] stamp = new int[1];
String val = ref.get(stamp);  // val="A", stamp=1

// T2:A→B→A,版本号变成 2→3
ref.compareAndSet("A", "B", 1, 2);
ref.compareAndSet("B", "A", 2, 3);

// T1:CAS,版本号不匹配,失败
ref.compareAndSet("A", "C", 1, 2);  // false

加了版本号,CAS 同时比较值和版本号,中间任何变化都会被检测到。

6.4 两种解决方案对比

方案 数据结构 适用场景
AtomicStampedReference 值 + int 版本号 精确追踪修改次数
AtomicMarkableReference 值 + boolean 标记 只需知道"改没改过"

七、踩坑总结

坑一:volatile 不是万能的

volatile 只能保证单次读写,复合操作(count++)不保证原子性。

// ❌ 错误
volatile int count = 0;
count++;  // 不安全

// ✅ 正确
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

坑二:自旋 CAS 的 CPU 消耗

高并发下大量线程自旋重试,CPU 100%。

解决方案:控制自旋次数,或者用 LongAdder(分段 CAS)替代 AtomicLong。

坑三:ABA 问题容易被忽略

链表操作、栈操作等场景下,ABA 会导致数据结构损坏。

解决方案:用 AtomicStampedReference 替代 AtomicReference。


八、知识串联

并发问题的根源:CPU 缓存导致可见性、指令重排导致有序性

JMM 规范:happens-before 规则定义了"什么样的顺序是合法的"

volatile:
  └─ 通过 LOCK 前缀 + MESI 协议,保证可见性 + 有序性
  └─ 不保证复合操作的原子性

CAS:
  └─ CPU 指令级别的原子操作,失败就重试
  └─ 底层靠 Unsafe 暴露给 Java

Unsafe:
  └─ 直接操作内存,CAS 的 Java 入口
  └─ native 方法,JVM 底层调汇编

AtomicInteger:
  └─ volatile value + Unsafe CAS + 自旋
  └─ 完整展示 CAS 的实际应用

ABA 问题:
  └─ 值相同但中间被改过
  └─ 用 AtomicStampedReference 加版本号解决

九、面试 Q&A

Q1:volatile 和 synchronized 的区别?

volatile synchronized
原子性 ❌ 不保证复合操作 ✅ 保证
可见性 ✅ 立即刷新主内存 ✅ happens-before
有序性 ✅ 禁止指令重排 ✅ 互斥保证有序
性能 高(无锁) 低(加锁)

Q2:CAS 的缺点?

  1. 自旋消耗 CPU
  2. 只能保证单变量原子性
  3. 存在 ABA 问题

Q3:AtomicInteger.incrementAndGet() 怎么实现的?

Unsafe 的 getAndAddInt,底层是自旋 CAS:读取当前值 → CAS 更新 → 失败重试。

Q4:ABA 问题怎么解决?

AtomicStampedReference 加版本号,或者 AtomicMarkableReference 加标记位。


十、口诀

JMM 三问题:可见性、原子性、有序性
volatile 保可见保有序,不保复合原子性
CAS 是 CPU 指令,失败就重试
Unsafe 是后门,native 调汇编
AtomicInteger = volatile + CAS + 自旋
ABA 问题不忽视,版本号来标记

搞懂 volatile 和 CAS,JUC 就入门了一半。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐