Java 原子类详解


一、从问题出发:为什么需要原子类?

要理解原子类,我们先从一个具体的问题开始。

假设我们有一个普通的整数变量,用来统计网站的访问次数:

int count = 0;
count++; // 每次有请求进来,计数加1

这段代码在单线程环境下完全没有问题。但在多线程环境下,count++ 这个看起来只有一步的操作,实际上在底层被拆分成了三个独立的步骤:

  1. 读取:从内存中读取 count 的当前值
  2. 计算:将读取到的值加 1
  3. 写回:将计算结果写回内存

当多个线程同时执行这三步时,就会出现问题。举个例子:

初始值:count = 0

线程A:读取 count = 0
线程B:读取 count = 0   ← 线程B在线程A写回之前也读取了
线程A:计算 0 + 1 = 1
线程B:计算 0 + 1 = 1
线程A:写回 count = 1
线程B:写回 count = 1   ← 覆盖了线程A的结果

最终结果:count = 1     ← 期望是 2,实际是 1,数据出错!

两个线程各自执行了一次加一操作,但最终结果却只加了一次。这就是线程安全问题。

传统的解决方案是使用 synchronized 关键字或 Lock 锁,但这些方案会让线程排队等待,在竞争激烈时性能损耗较大。

原子类就是为了解决这类问题而生的——它能在不加锁的情况下,保证操作的线程安全。


二、原子类是什么?

原子类是 Java 在 java.util.concurrent.atomic 包下提供的一组工具类。"原子"这个词来自物理学,意思是不可再分割的最小单位。在计算机领域,原子操作指的是一个操作要么完整地执行完毕,要么完全不执行,不存在执行到一半被打断的情况。

原子类提供的操作,就具备这种"原子性"——整个操作过程不会被其他线程干扰,从而保证了线程安全。

Java 提供了十几个原子类,按照操作对象的不同,分为四大类型:

类型代表类用途
原子更新基本类型AtomicInteger、AtomicLong、AtomicBoolean对整数、长整数、布尔值进行原子操作
原子更新数组AtomicIntegerArray、AtomicLongArray对数组中某个元素进行原子操作
原子更新引用AtomicReference、AtomicStampedReference对对象引用进行原子操作
原子更新属性AtomicIntegerFieldUpdater对某个对象的某个字段进行原子操作

三、原子类的核心原理:CAS 机制

3.1 CAS 是什么?

原子类能做到无锁且线程安全,核心依赖一种叫做 CAS(Compare-And-Swap,比较并交换) 的机制。

CAS 的逻辑非常直接,可以用下面的伪代码来描述:

// 伪代码描述CAS操作
boolean compareAndSwap(int* address, int expectedValue, int newValue) {
    if (*address == expectedValue) {  // 第一步:比较内存中的当前值与预期值
        *address = newValue;          // 第二步:如果一致,则更新为新值
        return true;                  // 更新成功
    }
    return false;                     // 如果不一致,说明有其他线程修改过,更新失败
}

CAS 接收三个参数:

  • address(内存地址):要修改的变量所在的内存位置
  • expectedValue(预期值):我认为这个变量当前应该是什么值
  • newValue(新值):我想把这个变量改成什么值

操作逻辑:先去内存里看一眼,如果当前值和我预期的一样,说明在我读取之后没有其他线程动过这个变量,那我就放心地把它改成新值;如果不一样,说明有其他线程已经修改过了,我的操作就取消,然后重新读取最新值再试一次。

3.2 CAS 如何解决线程安全问题?

我们用 CAS 来重新看 count++ 的过程,假设初始值 count = 0,线程A和线程B同时执行加一:

初始值:count = 0

线程A:读取当前值 = 0,准备执行 CAS(address, 0, 1)
线程B:读取当前值 = 0,准备执行 CAS(address, 0, 1)

线程A 先执行 CAS:
  检查内存中的值 == 预期值 0?  → 是的,相等
  将内存中的值更新为 1          → 成功!count = 1

线程B 后执行 CAS:
  检查内存中的值 == 预期值 0?  → 不对!内存中已经是 1 了
  更新失败                      → 重试!

线程B 重试:
  重新读取当前值 = 1,准备执行 CAS(address, 1, 2)
  检查内存中的值 == 预期值 1?  → 是的,相等
  将内存中的值更新为 2          → 成功!count = 2

最终结果:count = 2  ← 正确!

线程B 没有覆盖线程A的结果,而是检测到冲突后重新尝试,最终得到了正确的结果。

这种"检测到冲突就重试"的模式,叫做自旋(Spin)。整个过程没有任何锁,没有线程被阻塞,所以效率很高。

3.3 CAS 的原子性从哪里来?

你可能会问:CAS 本身也有"比较"和"交换"两个步骤,这两步之间会不会也被其他线程打断?

这是一个非常关键的问题,答案是:不会。

CAS 的原子性不是由 Java 代码保证的,而是直接由 CPU 硬件指令保证的。

整个调用链路是这样的:

Java 原子类方法
      ↓
Unsafe 类的 native 方法(Java调用底层C代码的桥梁)
      ↓
C 代码
      ↓
汇编指令
      ↓
CPU 原子指令 cmpxchg(在 x86 架构上)

最终生成的是 CPU 的 cmpxchg 指令(Compare and Exchange)。这条指令在 CPU 层面是不可分割的——CPU 在执行这条指令期间,会通过总线锁或缓存锁机制,确保没有其他 CPU 核心能同时访问同一块内存。

因此,CAS 的原子性是由硬件在最底层保证的,Java 只是通过 Unsafe 类将这个能力暴露出来供开发者使用。

3.4 Unsafe 类的角色

Unsafe 是 Java 中一个特殊的类,它提供了直接操作内存的能力,是 Java 与底层硬件之间的桥梁。原子类的核心方法都是通过 Unsafe 实现的:

// AtomicInteger 中的 incrementAndGet 方法
public final int incrementAndGet() {
    // 调用 Unsafe 的 getAndAddInt 方法
    // this:当前对象
    // valueOffset:value字段在内存中的偏移量(即内存地址)
    // 1:要加的值
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

getAndAddInt 内部通过循环 CAS 实现:不断尝试 CAS 操作,直到成功为止:

// Unsafe.getAndAddInt 的内部逻辑(简化版)
public final int getAndAddInt(Object obj, long offset, int delta) {
    int currentValue;
    do {
        currentValue = getIntVolatile(obj, offset); // 读取内存中的当前值
    } while (!compareAndSwapInt(obj, offset, currentValue, currentValue + delta));
    // 如果CAS失败(有其他线程修改了值),就循环重试
    return currentValue;
}

这个循环就是"自旋"的体现——失败了就一直重试,直到成功。


四、原子类的使用场景

4.1 什么情况下选择原子类?

原子类最适合的场景是:需要对单个变量进行简单的线程安全操作。

判断标准:

  • 操作简单:只是对一个变量做递增、递减、读取、设置等基础操作
  • 追求高效:不想因为加锁而让线程排队等待,希望减少性能损耗
  • 需要线程安全:在多线程环境下,要保证数据的正确性

4.2 典型使用示例:计数器

统计网站请求次数是原子类最经典的应用场景:

// 创建一个原子整数,初始值为 0
AtomicInteger requestCount = new AtomicInteger(0);

// 每次有请求进来,计数加1(线程安全)
requestCount.incrementAndGet();

// 获取当前计数值
int total = requestCount.get();
System.out.println("总请求数:" + total);

incrementAndGet() 方法内部使用 CAS 循环,保证了即使在高并发下,每一次加一操作都会被正确计入,不会出现前面描述的数据覆盖问题。

4.3 原子类 vs synchronized/Lock

同样是线程安全的计数器,用 synchronized 也可以实现:

// synchronized 实现
private int count = 0;
public synchronized void increment() {
    count++;
}

// 原子类实现
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet();
}

两者都能保证线程安全,但在简单变量操作这个场景下,原子类更合适,原因如下:

原子类(基于 CAS,无锁)的特点:

  • 线程不会被阻塞,不需要等待其他线程释放锁
  • 冲突时直接重试,没有线程切换的开销
  • 代码更简洁,语义更清晰

synchronized/Lock(基于锁)的特点:

  • 同一时刻只有一个线程能执行,其他线程必须等待(阻塞)
  • 线程阻塞和唤醒需要操作系统介入,有一定的性能开销
  • 适合保护一段复杂的代码逻辑(临界区),而不仅仅是单个变量

简单来说:操作单个变量用原子类,保护复杂逻辑代码块用锁。


五、完整知识结构梳理

通过以上内容,我们可以将原子类的知识体系整理如下:

Java 原子类
├── 是什么
│   ├── java.util.concurrent.atomic 包下的工具类
│   └── 提供无锁、线程安全的变量操作
│
├── 有哪些(4种类型)
│   ├── 原子更新基本类型(AtomicInteger、AtomicLong 等)
│   ├── 原子更新数组(AtomicIntegerArray 等)
│   ├── 原子更新引用(AtomicReference 等)
│   └── 原子更新属性(AtomicIntegerFieldUpdater 等)
│
├── 为什么是原子性的(原理)
│   ├── 核心:CAS(比较并交换)机制
│   │   ├── 读取当前值,与预期值比较
│   │   ├── 相等则更新,不等则重试(自旋)
│   │   └── 整个过程不可被打断
│   ├── 实现路径
│   │   └── Java方法 → Unsafe → C代码 → 汇编 → CPU原子指令(cmpxchg)
│   └── 原子性最终由 CPU 硬件指令保证
│
└── 什么时候用(使用场景)
    ├── 操作简单的单个变量
    ├── 高并发下需要线程安全
    └── 对比锁:无锁更高效,适合简单操作

六、总结

原子类解决的核心问题是:在多线程环境下,如何高效且安全地操作共享变量。

它的实现思路是:不依赖操作系统层面的锁机制,而是直接利用 CPU 提供的原子指令(通过 Unsafe 类调用),以 CAS 的方式实现"无锁编程"。当多个线程同时修改同一个变量时,CAS 机制保证只有一个线程能成功,其他线程检测到冲突后自动重试,最终所有操作都能被正确执行。

这种方式的优势在于:简单、高效、安全——代码简洁,没有锁的阻塞开销,同时在多线程下数据始终正确。因此,在需要对单个变量进行线程安全操作(如计数器、统计量)的场景下,原子类是比 synchronized 和 Lock 更合适的选择。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐