基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Atomic 原子类(如 AtomicInteger)的实现原理

知识点图片

Atomic原子类通过 volatile 保证内存可见性,并利用 Unsafe 类的 CAS(比较并交换)乐观锁机制,以自旋方式高效地实现了无锁的原子操作。

我们来深入探讨一下 Java 中 Atomic 原子类的实现原理,并以最常用的 AtomicInteger 为例。

一句话总结其核心原理就是:通过 volatile 保证内存可见性,通过 CAS (Compare-And-Swap) 乐观锁机制保证原子性,而这两者的底层实现都依赖于 sun.misc.Unsafe 类来直接操作内存和调用 CPU 指令。

下面我们来分步拆解这个原理。


1. 问题背景:为什么需要原子类?

在多线程环境下,对一个共享变量进行 i++ 这样的操作不是原子性的。它至少包含三个步骤:

  1. 读取 i 的当前值。
  2. 将读取到的值 加 1
  3. 将计算后的结果 写回 i

在并发场景下,多个线程可能同时执行到第一步,读取到相同的值,然后各自加一再写回,导致最终结果小于预期,这就是线程安全问题。

通常的解决方案是使用 synchronized 关键字或 Lock

java
public synchronized void increment() {
    count++;
}

这种方式被称为悲观锁,它假设总会有冲突发生,所以每次操作前都先加锁,阻塞其他线程。虽然保证了安全,但在竞争不激烈的情况下,加锁和解锁的开销(涉及用户态和内核态的切换)会造成性能瓶न्दा。

原子类提供了一种无锁 (Lock-Free) 的、性能更高的替代方案。


2. 核心组件一:volatile 关键字 - 保证可见性

我们打开 AtomicInteger 的源码,首先会看到它的核心成员变量:

java
public class AtomicInteger extends Number implements java.io.Serializable {
    // ...
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset; // 内存偏移地址

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value; // 核心!
    // ...
}

这里的 value 变量被 volatile 修饰。volatile 关键字主要有两个作用:

  1. 保证可见性 (Visibility):当一个线程修改了 value 的值,这个修改会立刻被刷新到主内存中。同时,其他线程在读取 value 之前,会先使自己的本地缓存失效,强制从主内存中重新读取最新值。这就确保了所有线程看到的 value 始终是一致的。

  2. 禁止指令重排序 (Happens-Before)volatile 会插入内存屏障,防止编译器和处理器为了优化而随意改变代码的执行顺序,确保代码的逻辑正确性。

可见性是 CAS 操作成功的基础。 如果没有 volatile,一个线程可能基于一个过期的值去执行 CAS 操作,导致逻辑错误。


3. 核心组件二:CAS (Compare-And-Swap) - 保证原子性

CAS 是原子类实现原子操作的基石。它是一种乐观锁机制。

CAS 是什么?

CAS 是一种硬件级别的原子指令,它接受三个参数:

  1. V (Variable):要更新的内存地址(或变量)。
  2. A (Assumed/Expected):期望的旧值。
  3. B (New Value):要更新的新值。

CAS 操作的逻辑是:“我认为内存地址 V 的值应该是 A,如果是,那就把它更新为 B;如果不是,说明在我准备更新的期间有其他线程已经修改了它,那我就什么都不做,并返回失败。”

这个 "比较并交换" 的过程是一条 CPU 原子指令(例如 x86 架构下的 CMPXCHG 指令),由硬件保证其不可中断,从而实现了原子性。

CAS 如何在 AtomicInteger 中工作?

我们以 AtomicInteger.getAndIncrement() 方法(等同于 i++)为例,看看它的简化版源码:

java
public final int getAndIncrement() {
    // this 是当前 AtomicInteger 对象
    // valueOffset 是 value 字段的内存偏移地址
    // 1 是要增加的值
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe.java 中的 getAndAddInt 方法
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        // 1. 读取当前 volatile 变量的值
        v = getIntVolatile(o, offset); 
    } while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. CAS 尝试更新
    return v; // 3. 返回更新前的值
}

这个 do-while 循环就是 CAS 的经典使用模式:

  1. 读取 (Read):通过 getIntVolatile 读取 value 的当前值(假设为 v)。
  2. 计算 (Modify):在本地计算出新值 v + delta(这里 delta 是 1)。
  3. 交换 (Compare-And-Swap):调用 compareAndSwapInt 方法,尝试将 valuev 更新为 v + 1
    • 如果成功:说明从读取 v 到现在,没有其他线程修改过 value。更新完成,循环结束。
    • 如果失败:说明在步骤 1 和 3 之间,有其他线程抢先修改了 value。此时 compareAndSwapInt 返回 false,循环继续。
    • 重试:循环回到步骤 1,重新读取最新的 value 值,然后再次尝试,直到成功为止。

这个不断重试的过程,也叫自旋 (Spinning)。由于大部分情况下线程竞争不激烈,CAS 操作一次就能成功,所以性能远高于需要操作系统介入的悲观锁。


4. 幕后推手:sun.misc.Unsafe

Java 本身无法直接调用 CPU 的 CAS 指令。它通过一个名为 sun.misc.Unsafe 的特殊类来实现。这个类提供了类似 C++ 指针的、直接操作内存的能力。

AtomicInteger 在静态代码块中,通过反射获取 Unsafe 实例,并计算出 value 字段在对象内存布局中的偏移地址 (offset)

之后,所有对 value 的操作,如 compareAndSwapInt,都是通过 Unsafe 的方法,传入对象实例、字段偏移地址和要操作的值来完成的。Unsafe 类的方法在底层会调用 JVM 的内部方法,最终映射到 CPU 的原子指令上。


5. CAS 的 ABA 问题

CAS 机制并非完美,它存在一个经典的 "ABA" 问题。

  • 问题描述
    1. 线程 T1 读取内存值 V 为 A。
    2. 线程 T2 介入,将 V 的值从 A 改为 B,然后又改回 A。
    3. 线程 T1 执行 CAS 操作,发现内存值 V 仍然是 A,于是成功更新。

对于 T1 来说,它没有意识到值虽然没变,但状态其实已经发生过变化。在某些业务场景下(例如链表操作),这可能会导致严重问题。

  • 解决方案
    Java 提供了 AtomicStampedReference 类来解决 ABA 问题。它的原理是为每个值增加一个版本号(或叫“戳”,Stamp)。CAS 操作时不仅要比较值,还要比较版本号。当值被修改时,版本号也随之改变。这样,即使值从 A -> B -> A,版本号也已经从 1 -> 2 -> 3,T1 的 CAS 操作会因为版本号不匹配而失败。

6. 高并发下的性能瓶颈与 LongAdder

在高并发场景下,如果大量线程同时更新同一个 AtomicInteger,会导致 CAS 操作的失败率急剧上升。大量线程会陷入自旋等待,不断重试,消耗大量 CPU 资源,性能反而会下降。

为了解决这个问题,Java 8 引入了 LongAdder

  • LongAdder 的原理分段锁/数据分离
    • 它内部维护一个 base 变量和一个 Cell[] 数组。
    • 在没有竞争或竞争很低时,直接通过 CAS 更新 base 值,和 AtomicLong 类似。
    • 当更新 base 失败(出现竞争)时,线程会尝试去更新 Cell 数组中某个槽(Cell)的值。每个线程通过哈希等算法映射到自己的 Cell 上,从而将竞争分散到多个 Cell 中。
    • 获取总和时(调用 sum() 方法),再将 base 和所有 Cell 的值累加起来。

LongAdder空间换时间的思想,极大地减少了热点更新的竞争,使得其在统计、计数等高并发场景下的吞吐量远超 AtomicLong。但它不适合需要强一致性的、依赖于 CAS 返回值的场景。


总结

特性 实现方式 作用
可见性 private volatile int value; 确保多线程间对 value 值的修改能被立即看到。
原子性 Unsafe.compareAndSwapInt() 利用 CPU 的原子指令,实现无锁的、乐观的原子更新。
机制 自旋循环 (Spinning Loop) 当 CAS 失败时,不断重试,直到成功。
底层桥梁 sun.misc.Unsafe 连接 Java 代码和底层硬件指令,提供直接内存操作能力。

Atomic 原子类的实现是现代并发编程中一个非常经典的例子,它巧妙地利用了硬件特性,避免了传统锁的性能开销,为我们提供了一个高效、轻量级的并发工具。

00:00
00:00