Java 原子类详解
Java 原子类详解
一、从问题出发:为什么需要原子类?
要理解原子类,我们先从一个具体的问题开始。
假设我们有一个普通的整数变量,用来统计网站的访问次数:
int count = 0;
count++; // 每次有请求进来,计数加1
这段代码在单线程环境下完全没有问题。但在多线程环境下,count++ 这个看起来只有一步的操作,实际上在底层被拆分成了三个独立的步骤:
- 读取:从内存中读取
count的当前值 - 计算:将读取到的值加 1
- 写回:将计算结果写回内存
当多个线程同时执行这三步时,就会出现问题。举个例子:
初始值: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 更合适的选择。
更多推荐


所有评论(0)