Java 原子类(AtomicInteger/AtomicReference):无锁并发编程实战
引言
你以为用了 synchronized 就能搞定所有并发计数问题?为什么同样是统计接口调用量,测试环境数据准确,生产高并发下就总是少统计?我曾经在电商订单项目中踩过致命坑:用普通 int 变量做并发计数,结果大促当天订单数统计少了 3000 多单,对账时才发现数据失真;还有次用 synchronized 加锁计数,虽然数据对了,但接口响应时间从 20ms 飙升到 150ms,吞吐量直接腰斩。
很多开发者都有个误区:要么觉得普通变量加个锁就万事大吉,要么听说原子类高效就盲目乱用,根本不懂底层逻辑。其实在高并发场景下,原子类才是兼顾线程安全和性能的最优解之一。读完这篇,你能吃透 AtomicInteger、AtomicReference 的核心用法,分清哪些场景该用原子类、哪些该用锁,还能避开原子类使用中的各种陷阱,写出生产级的无锁并发代码。
一、从并发计数 bug 开始:为什么普通变量不行?
我发现 80% 的新手刚接触并发时,都会犯一个低级错误:用普通 int/long 变量做并发修改,觉得只要加个简单的自增就没问题。甚至有人觉得 “就加个 1 而已,能出什么错?”
java
运行
// 新手错误写法:普通变量做并发计数
public class WrongCounter {
private int count = 0;
// 自增方法,无任何线程安全措施
public void increment() {
count++; // 看似简单,实则非原子操作
}
public int getCount() {
return count;
}
}
我在用户行为统计项目中见过这段代码,上线后发现统计数据总是比实际少,高峰期每小时少统计上千次点击。后来排查才知道,count++ 看似是一行代码,实则包含 “读取 - 修改 - 写入” 三个步骤,并发时多个线程会同时读取同一个值,修改后再写入,导致数据覆盖。
说白了,普通变量的自增、自减操作都不是原子性的,在多线程环境下会出现竞态条件(术语:竞态条件,指多个线程同时访问共享资源,最终结果依赖于线程执行顺序的情况)。就像两个人同时往一个存钱罐里放钱,都先看了一眼里面有 100 元,然后各自放 50 元,最后本该有 200 元,结果却只有 150 元 —— 这就是并发修改导致的数据失真。
java
运行
// 稍微改进但仍有问题的写法:synchronized加锁
public class SynchronizedCounter {
private int count = 0;
// 加锁保证原子性,但性能差
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
这种写法虽然能保证数据准确,但 synchronized 是独占锁,同一时间只有一个线程能执行,高并发下会出现大量线程阻塞等待。我曾经把这个计数器用在首页接口统计上,结果接口吞吐量从 1000QPS 降到 200QPS,响应时间暴涨,这就是过度加锁的代价。
而 AtomicInteger 的核心优势就是:在无锁的情况下保证 “读取 - 修改 - 写入” 的原子性,性能比 synchronized 高得多。
二、为什么原子类比 synchronized 更高效?
曾经我也以为原子类是 “黑科技”,直到深入了解底层原理才明白,它的核心是 CAS 机制。很多人容易理解错 CAS,觉得它是一种锁,其实它是一种无锁算法。
术语:CAS(Compare and Swap,比较并交换),核心逻辑是 “先比较当前值是否和预期值一致,如果一致就更新为目标值,否则不做操作”,整个过程是原子性的,由 CPU 指令直接支持。
可以用一个通俗的比喻理解 CAS:你去超市买水,货架上标价 2 元(预期值),你准备付款时先确认价格还是 2 元,确认无误就付款拿走(更新操作);如果价格变了(被其他线程修改),你就放弃当前操作,重新确认价格再尝试 —— 这就是 CAS 的 “自旋重试” 逻辑。
而 synchronized 是独占锁,相当于超市里某个商品只能一个人买,其他人必须排队等前面的人买完。高并发下,排队的人越多,效率越低;而 CAS 是无锁机制,多个线程可以同时尝试修改,不需要排队,只有当发现值被修改时才重试,所以性能更优。
下面用代码对比原子类和 synchronized 的性能差异,这是我在本地压测的真实数据(Java 17,8 核 CPU):
java
运行
// 性能对比测试代码(Java 17+)
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PerformanceCompare {
private static final int THREAD_COUNT = 10;
private static final int LOOP_COUNT = 1000000;
public static void main(String[] args) throws InterruptedException {
// 测试synchronized计数器
SynchronizedCounter syncCounter = new SynchronizedCounter();
long syncStartTime = System.currentTimeMillis();
testCounter(syncCounter);
long syncCost = System.currentTimeMillis() - syncStartTime;
// 测试AtomicInteger计数器
AtomicInteger atomicCounter = new AtomicInteger(0);
long atomicStartTime = System.currentTimeMillis();
testAtomicCounter(atomicCounter);
long atomicCost = System.currentTimeMillis() - atomicStartTime;
System.out.println("synchronized计数耗时:" + syncCost + "ms");
System.out.println("AtomicInteger计数耗时:" + atomicCost + "ms");
}
private static void testCounter(SynchronizedCounter counter) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.increment();
}
latch.countDown();
});
}
latch.await();
executor.shutdown();
}
private static void testAtomicCounter(AtomicInteger counter) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
counter.incrementAndGet(); // 原子自增
}
latch.countDown();
});
}
latch.await();
executor.shutdown();
}
}
✅ 压测结果:
- synchronized 计数耗时:128ms
- AtomicInteger 计数耗时:32ms可以明显看到,AtomicInteger 的性能是 synchronized 的 4 倍左右。这也是为什么在高并发计数、统计等场景,原子类是首选。
三、从 AtomicInteger 到 AtomicReference:对象级原子性怎么保证?
很多新手用会了 AtomicInteger,就以为所有原子类用法都一样,直到遇到对象修改的场景才懵了。我在用户状态管理项目中见过这种错误:用 AtomicReference 包装用户对象,却试图修改对象内部的属性,结果导致状态混乱。
java
运行
// 新手错误写法:修改AtomicReference包装对象的内部属性
public class WrongAtomicReference {
// 用AtomicReference包装User对象
private static AtomicReference<User> userRef = new AtomicReference<>(new User("张三", 0));
public static void main(String[] args) {
// 错误:直接修改对象内部属性,非原子操作
User user = userRef.get();
user.setScore(user.getScore() + 10); // 这里会有并发问题
userRef.set(user);
}
static class User {
private String name;
private int score;
// 构造器、getter、setter省略
}
}
这个错误的核心原因是:AtomicReference 只能保证 “对象引用的修改” 是原子性的,不能保证 “对象内部属性的修改” 是原子性的。就像你用一个安全的盒子装了一个笔记本,盒子是安全的(引用不可随意修改),但笔记本里的内容还是能被多人同时修改 —— 这就是很多人容易理解错的点。
正确的做法是:要么用不可变对象(修改时创建新对象),要么用专门的原子对象类(如 AtomicStampedReference)。不可变对象就像每次修改笔记本内容时,都重新写一本新的笔记本放进盒子,这样就不会出现并发修改问题。
四、实战代码:从基础用法到生产级实践
示例 1:基础用法 ——AtomicInteger 核心操作(Java 17+)
完整可运行代码,覆盖原子自增、自减、比较并设置等核心方法,关键行标注为什么这么写。
java
运行
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerBasicDemo {
public static void main(String[] args) {
// 初始化原子整数,初始值为0
AtomicInteger atomicInt = new AtomicInteger(0);
// 1. 原子自增(i++),返回自增前的值
int prevValue = atomicInt.getAndIncrement();
System.out.println("自增前值:" + prevValue + ",当前值:" + atomicInt.get()); // 0,1
// 2. 原子自增(++i),返回自增后的值
int currValue = atomicInt.incrementAndGet();
System.out.println("自增后值:" + currValue); // 2
// 3. 原子自减(i--),返回自减前的值
prevValue = atomicInt.getAndDecrement();
System.out.println("自减前值:" + prevValue + ",当前值:" + atomicInt.get()); // 2,1
// 4. 比较并设置(CAS核心操作):预期值为1时,更新为10
boolean success = atomicInt.compareAndSet(1, 10);
System.out.println("CAS更新是否成功:" + success + ",更新后值:" + atomicInt.get()); // true,10
// 5. 原子累加指定值,返回累加后的值
currValue = atomicInt.addAndGet(5);
System.out.println("累加5后值:" + currValue); // 15
}
}
✅ 执行结果:自增前值:0,当前值:1自增后值:2自减前值:2,当前值:1CAS 更新是否成功:true,更新后值:10累加 5 后值:15💡 要点:getAndIncrement(先拿后加)和 incrementAndGet(先加后拿)是最常用的两个方法,根据业务需要选择;compareAndSet 是 CAS 的核心方法,返回值表示是否更新成功。
示例 2:进阶用法 —— 生产环境原子类常见模式(统计 + 状态管理)
这是我在电商项目中实际用过的两个模式:接口调用量统计、用户状态原子更新,直接复用即可。
java
运行
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
// 模式1:接口调用量统计(高并发场景)
class ApiCounter {
// 原子整数统计调用次数,初始值0
private final AtomicInteger callCount = new AtomicInteger(0);
// 原子整数统计失败次数
private final AtomicInteger failCount = new AtomicInteger(0);
// 接口调用成功时调用
public void incrementCallCount() {
callCount.incrementAndGet();
}
// 接口调用失败时调用
public void incrementFailCount() {
failCount.incrementAndGet();
}
// 获取统计数据(线程安全)
public Stats getStats() {
// 用record存储统计结果(Java 16+特性,不可变对象)
return new Stats(callCount.get(), failCount.get());
}
// 统计结果实体(不可变)
public record Stats(int callCount, int failCount) {}
}
// 模式2:用户状态原子更新(不可变对象模式)
class UserStatusManager {
// 用AtomicReference包装不可变User对象
private final AtomicReference<User> userRef = new AtomicReference<>(new User("默认用户", 0));
// 原子更新用户分数:创建新对象替换旧对象
public void updateScore(int addScore) {
while (true) {
// 获取当前用户对象
User currentUser = userRef.get();
// 创建新用户对象(不可变,避免并发修改)
User newUser = new User(currentUser.name(), currentUser.score() + addScore);
// CAS更新:如果当前引用没变,就替换为新对象
if (userRef.compareAndSet(currentUser, newUser)) {
break; // 更新成功,退出循环
}
// 更新失败,自旋重试
}
}
// 不可变用户类(所有属性final,无setter)
public record User(String name, int score) {}
}
// 用法演示
public class AtomicAdvancedDemo {
public static void main(String[] args) {
ApiCounter counter = new ApiCounter();
counter.incrementCallCount();
counter.incrementFailCount();
System.out.println("接口统计:" + counter.getStats().callCount() + "次调用," + counter.getStats().failCount() + "次失败");
UserStatusManager manager = new UserStatusManager();
manager.updateScore(20);
System.out.println("用户分数:" + manager.userRef.get().score()); // 20
}
}
💡 模式优势:1. 统计类用两个 AtomicInteger 分别统计成功和失败次数,避免锁竞争,性能优异;2. 状态管理用不可变对象 + AtomicReference,保证对象级原子性,避免内部属性并发修改问题;3. 用 record(Java 16+)定义不可变对象,代码简洁且线程安全。
示例 3:踩坑示范 —— 原子类方法叠加使用的陷阱
我在风控项目中见过这段代码,看似用了 AtomicInteger 保证安全,实则存在并发问题,导致风控规则判断错误。
java
运行
// 错误代码:原子类方法叠加使用,非原子操作
public class Atomic叠加错误Demo {
private final AtomicInteger count = new AtomicInteger(0);
// 错误:get()和addAndGet()分开调用,中间存在间隙
public int addIfLessThan100(int add) {
int current = count.get(); // 1. 读取当前值
// 高并发下,这里可能被其他线程修改
if (current < 100) {
return count.addAndGet(add); // 2. 累加
}
return current;
}
}
❌ 为什么错:虽然 get () 和 addAndGet () 都是原子操作,但把它们拆分开后,整个 “读取 - 判断 - 累加” 的流程就不是原子性的了。比如线程 A 读取 current=99,准备累加时,线程 B 也读取 current=99 并完成累加,此时 count 变成 100,线程 A 再累加就会超过 100,违反 “小于 100 才累加” 的业务规则。❌ 后果:我在风控项目中,这个错误导致超过阈值的请求没有被拦截,出现了风险漏洞。
✅ 正确做法:用 CAS 自旋或者 AtomicInteger 的 accumulateAndGet 方法,保证整个流程的原子性。
java
运行
// 正确代码:用CAS自旋保证流程原子性
public class AtomicCorrectDemo {
private final AtomicInteger count = new AtomicInteger(0);
public int addIfLessThan100(int add) {
while (true) {
int current = count.get();
if (current >= 100) {
return current;
}
// CAS更新:当前值还是current时,才累加add
if (count.compareAndSet(current, current + add)) {
return current + add;
}
// 更新失败,自旋重试
}
}
}
示例 4:最佳实践 —— 生产级原子类工具类(Java 17+)
整合 AtomicInteger、AtomicReference 的核心用法,封装成生产可用的工具类,兼顾线程安全、性能和可维护性。
java
运行
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;
/**
* 生产级原子类工具类:封装计数、状态更新等常用操作
* 特点:线程安全、无锁高效、代码简洁、支持自定义更新逻辑
*/
public class AtomicUtils {
// 私有构造器,禁止实例化
private AtomicUtils() {}
// ------------------- AtomicInteger工具方法 -------------------
/**
* 原子计数自增(先加后拿)
*/
public static int increment(AtomicInteger atomicInt) {
if (atomicInt == null) {
throw new IllegalArgumentException("原子整数不能为null");
}
return atomicInt.incrementAndGet();
}
/**
* 原子计数自减(先减后拿)
*/
public static int decrement(AtomicInteger atomicInt) {
if (atomicInt == null) {
throw new IllegalArgumentException("原子整数不能为null");
}
return atomicInt.decrementAndGet();
}
/**
* 自定义原子更新(支持复杂逻辑)
*/
public static int update(AtomicInteger atomicInt, IntUnaryOperator updateFunction) {
if (atomicInt == null || updateFunction == null) {
throw new IllegalArgumentException("参数不能为null");
}
return atomicInt.updateAndGet(updateFunction);
}
// ------------------- AtomicReference工具方法 -------------------
/**
* 原子更新引用(不可变对象模式)
*/
public static <T> T updateReference(AtomicReference<T> atomicRef, UnaryOperator<T> updateFunction) {
if (atomicRef == null || updateFunction == null) {
throw new IllegalArgumentException("参数不能为null");
}
return atomicRef.updateAndGet(updateFunction);
}
// 用法演示
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger(0);
AtomicUtils.increment(counter);
System.out.println("计数:" + counter.get()); // 1
// 自定义更新:大于10则设为10,否则加2
AtomicUtils.update(counter, (x) -> x > 10 ? 10 : x + 2);
System.out.println("自定义更新后:" + counter.get()); // 3
AtomicReference<User> userRef = new AtomicReference<>(new User("李四", 0));
// 原子更新用户分数
AtomicUtils.updateReference(userRef, user -> new User(user.name(), user.score() + 30));
System.out.println("用户分数:" + userRef.get().score()); // 30
}
// 不可变用户类
public record User(String name, int score) {}
}
✅ 工具类优势:1. 封装常用操作,避免重复编码,提高可维护性;2. 增加参数校验,避免空指针等基础错误;3. 支持自定义更新逻辑,适配复杂业务场景;4. 无锁设计,性能优异,可直接用于高并发生产环境。
五、易错点与避坑指南(都是我踩过的真实生产 bug)
❌ 常见错误 1:原子类方法叠加使用,破坏原子性
- 错误代码:
java
运行
private AtomicInteger count = new AtomicInteger(0);
public void add(int num) {
if (count.get() < 100) { // 读取
count.addAndGet(num); // 累加
}
}
- 实际场景:我在风控项目中用这段代码控制请求阈值,结果高并发下超过 100 的请求也被放行,出现风险漏洞,排查了 2 天才定位到问题。
- 根本原因:单个原子方法是原子的,但多个原子方法组合后,整个流程就不是原子的了。读取和累加之间存在时间间隙,可能被其他线程修改,导致判断条件失效。
- ✅ 正确做法:用 CAS 自旋或者 updateAndGet 方法,将整个流程封装成一个原子操作(参考示例 3 的正确代码)。
- 防守方案:封装原子类操作时,尽量保证 “一个方法对应一个原子操作”,避免多个原子方法在一个业务逻辑中组合使用。
❌ 常见错误 2:用 AtomicReference 修改可变对象内部属性
- 错误代码:
java
运行
private AtomicReference<User> userRef = new AtomicReference<>(new User("王五", 0));
public void updateScore(int add) {
User user = userRef.get();
user.setScore(user.getScore() + add); // 修改内部属性
userRef.set(user);
}
class User { // 可变对象,有setter
private String name;
private int score;
// 构造器、getter、setter
}
- 实际场景:用户积分项目中用这段代码更新积分,结果出现积分错乱,有的用户积分多加,有的少加。后来发现是多个线程同时修改同一个 User 对象的 score 属性导致的。
- 根本原因:AtomicReference 只能保证 “引用的更新” 是原子的,不能保证 “对象内部属性的修改” 是原子的。可变对象的内部属性修改本身就不是线程安全的,即使包装了 AtomicReference 也没用。
- ✅ 正确做法:使用不可变对象(如 record),修改时创建新对象替换旧对象;如果必须用可变对象,需给对象内部属性的修改加锁。
- 防守方案:用 AtomicReference 时,优先搭配不可变对象使用,从根源上避免内部属性并发修改问题。
❌ 常见错误 3:忽视 CAS 的 ABA 问题
- 错误代码:
java
运行
private AtomicInteger value = new AtomicInteger(10);
// 线程A:将10改为20
public void changeTo20() {
value.compareAndSet(10, 20);
}
// 线程B:将10改为30,再改回10
public void changeAndRestore() {
int current = value.get();
value.compareAndSet(current, 30);
// 做一些操作
value.compareAndSet(30, 10);
}
- 实际场景:我在库存管理项目中见过这个问题,线程 B 先修改库存再恢复,导致线程 A 误以为库存还是初始值,错误地执行了更新操作,导致库存超卖。
- 根本原因:这就是 CAS 的 ABA 问题(术语:ABA 问题,指一个值从 A 变成 B,再变回 A,CAS 会误以为值没变化而更新)。在有数据修改、恢复的场景下,ABA 问题会导致 CAS 判断失效。
- ✅ 正确做法:用 AtomicStampedReference(带版本号的原子引用),每次修改都更新版本号,CAS 时同时比较值和版本号。
java
运行
// 正确代码:用AtomicStampedReference解决ABA问题
private AtomicStampedReference<Integer> valueRef = new AtomicStampedReference<>(10, 0);
public void changeTo20() {
int[] stamp = new int[1];
int current = valueRef.get(stamp); // 获取当前值和版本号
// 同时比较值和版本号
valueRef.compareAndSet(current, 20, stamp[0], stamp[0] + 1);
}
- 防守方案:涉及数据修改、恢复的场景,禁止用普通 AtomicInteger/AtomicReference,必须用 AtomicStampedReference 带版本号校验。
❌ 常见错误 4:原子类与锁混用,性能抵消
- 错误代码:
java
运行
private AtomicInteger count = new AtomicInteger(0);
public synchronized void increment() {
count.incrementAndGet(); // 原子类+锁,多此一举
}
- 实际场景:我在新手同事的代码中见过这种写法,他觉得 “原子类加锁更安全”,结果导致接口响应时间比单纯用锁还长,吞吐量下降。
- 根本原因:原子类的优势是无锁高效,而 synchronized 是独占锁,两者混用会让原子类的无锁优势完全抵消,还会增加额外的性能开销(原子类的 CAS 操作本身也有开销)。
- ✅ 正确做法:二选一即可,高并发场景用原子类,需要复杂同步逻辑(如多个资源协调)用锁。
- 防守方案:代码评审时重点检查原子类使用场景,禁止原子类与锁不必要的混用。
❌ 常见错误 5:盲目用原子类替代锁,忽视复杂场景
- 错误代码:
java
运行
// 错误:用多个原子类协调多个资源,逻辑混乱且不安全
private AtomicInteger stock = new AtomicInteger(100);
private AtomicInteger money = new AtomicInteger(0);
public void buy() {
if (stock.get() > 0) {
stock.decrementAndGet();
money.addAndGet(100);
}
}
- 实际场景:电商库存项目中用这段代码处理下单,结果出现 “库存减少但钱没增加” 的情况。因为 stock 和 money 的修改是两个独立的原子操作,无法保证同时成功或同时失败。
- 根本原因:原子类只能保证单个变量的原子操作,无法保证多个变量的事务一致性。复杂的多资源协调场景,原子类无法替代锁(如 synchronized、ReentrantLock)或事务。
- ✅ 正确做法:用锁保证多个资源修改的原子性,或用分布式事务(分布式场景)。
java
运行
// 正确代码:用锁保证多资源修改的原子性
private int stock = 100;
private int money = 0;
public synchronized void buy() {
if (stock > 0) {
stock--;
money += 100;
}
}
- 防守方案:先明确业务场景,单个变量的并发修改用原子类,多个变量或复杂同步逻辑用锁。
六、总结与延伸
3 个核心要点
- 原子类基于 CAS 无锁机制,比 synchronized 更高效,适合单个变量的并发修改场景;
- 原子类只能保证单个操作的原子性,多个原子操作组合需用 CAS 自旋或锁;
- AtomicReference 需搭配不可变对象使用,避免修改内部属性导致的并发问题。
2 个延伸学习方向
- 深入学习 CAS 底层原理,理解 CPU 指令如何保证原子性,以及 ABA 问题的完整解决方案;
- 学习 Java 21 的 VarHandle,它是比原子类更底层的并发工具,支持更灵活的内存操作。
4 个面试高频提问 + 简洁答案
- 原子类的底层实现原理是什么?→ 基于 CAS(比较并交换)无锁算法,由 CPU 的 cmpxchg 指令保证原子性;
- 原子类和 synchronized 的区别?→ 原子类无锁,性能高,适合单个变量;synchronized 是独占锁,适合复杂同步逻辑,可保证多变量一致性;
- 什么是 ABA 问题?如何解决?→ 值从 A 变 B 再变 A,CAS 误判为未修改;用 AtomicStampedReference 加版本号校验;
- AtomicReference 的正确使用姿势?→ 搭配不可变对象,修改时创建新对象替换旧对象,避免修改内部属性。
更多推荐


所有评论(0)