面试被问原理答不上来,往往不是代码没写过,而是底层逻辑没吃透。很多开发者对数一的理解停留在“把1变成0”,却忽略了它在并发场景下的原子性保障。想要掌握这一核心机制的最佳实践,必须深入源码,理解JVM层面的内存模型。
项目目标
我们要搭建一个高并发的库存扣减系统,核心挑战在于如何保证在多线程环境下,库存数量不会出现负数或超卖。传统方案使用 int 或 long 类型配合 synchronized 锁,虽然可行,但性能瓶颈明显。本项目的目标是使用 AtomicInteger 实现无锁化的原子操作,模拟真实的秒杀场景。
我们将实现一个简易的秒杀服务,包含以下核心功能:初始化库存数量。
模拟高并发用户抢购。
实时返回抢购结果(成功/失败)。
统计最终库存剩余量。通过这个项目,你将理解 Java 内存模型中 volatile 关键字的作用,以及 Unsafe 类底层如何保证原子性。这不仅是语法糖,更是解决分布式系统一致性问题的重要基石。
目录结构
为了保持工程化规范,我们采用 Maven 标准结构。项目结构清晰,便于后续扩展为 Spring Boot 应用。
seckill-atomic-demo
├── pom.xml
├── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── seckill
│ │ ├── Main.java # 程序入口
│ │ ├── model
│ │ │ └── Product.java # 商品实体
│ │ └── service
│ │ └── SeckillService.java # 核心秒杀逻辑
│ └── test
│ └── java
│ └── com
│ └── example
│ └── seckill
│ └── SeckillServiceTest.java # 单元测试
└── README.mdpom.xml 中仅引入 JUnit 5 用于测试,无需额外依赖,保证环境纯净。这种轻量级结构适合快速验证核心算法逻辑,避免被框架配置分散注意力。
核心代码实现
商品实体类
定义一个简单的商品类,使用 AtomicInteger 存储库存。
package com.example.seckill.model;import java.util.concurrent.atomic.AtomicInteger;public class Product {private String name;// 使用 AtomicInteger 保证线程安全private final AtomicInteger stock;public Product(String name, int initialStock) {this.name = name;this.stock = new AtomicInteger(initialStock);}public String getName() {return name;}// 获取当前库存public int getStock() {return stock.get();}// 尝试扣减库存,核心方法public boolean decrementStock() {// 这里不能直接用 stock.decrementAndGet()// 因为可能减到负数,需要 CAS 循环while (true) {int current = stock.get();if (current = 0) {return false; // 库存不足}// 比较并交换:如果当前值还是 current,则更新为 current - 1if (stock.compareAndSet(current, current - 1)) {return true; // 扣减成功}// 如果 CAS 失败,说明被其他线程修改,继续循环重试}}
}关键解析:为什么不用 synchronized? 锁会有上下文切换开销,高并发下性能下降显著。
CAS 原理: compareAndSet(expected, update) 是原子操作。如果内存中的值等于 expected,则更新为 update,并返回 true;否则返回 false。
自旋锁思想: 通过 while 循环不断重试,直到 CAS 成功或库存为 0。这种乐观锁策略在竞争不激烈时效率极高。秒杀服务类
模拟高并发调用逻辑。
package com.example.seckill.service;import com.example.seckill.model.Product;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;public class SeckillService {private final Product product;private final ExecutorService executor;// 用于统计成功抢购数量private final AtomicInteger successCount = new AtomicInteger(0);public SeckillService(Product product) {this.product = product;// 创建固定大小线程池,模拟用户线程this.executor = Executors.newFixedThreadPool(100);}public void startSeckill(int totalUsers) {System.out.println(秒杀开始,初始库存: + product.getStock());long startTime = System.currentTimeMillis();for (int i = 0; i totalUsers; i++) {final int userId = i;executor.submit(() - {boolean result = product.decrementStock();if (result) {successCount.incrementAndGet();// 实际业务中这里会生成订单,这里简化处理}});}// 关闭线程池并等待任务完成executor.shutdown();while (!executor.isTerminated()) {Thread.yield(); // 让出 CPU,避免忙等待}long duration = System.currentTimeMillis() - startTime;System.out.println(秒杀结束,耗时: + duration + ms);System.out.println(成功抢购数: + successCount.get());System.out.println(剩余库存: + product.getStock());// 验证数据一致性if (successCount.get() + product.getStock() != 100) {System.err.println(数据不一致!发生超卖或丢单);} else {System.out.println(数据一致性校验通过);}}
}代码细节:线程池复用: 避免频繁创建销毁线程,模拟真实 Web 服务器行为。
数据校验: 成功数 + 剩余库存必须等于初始库存,这是验收测试的关键指标。
非阻塞等待: Thread.yield() 比 sleep 更合理,因为它只是建议调度器切换线程,不保证具体暂停时间。运行与测试
单元测试
编写测试用例验证并发安全性。
package com.example.seckill;import com.example.seckill.model.Product;
import com.example.seckill.service.SeckillService;
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class SeckillServiceTest {@Testvoid testSeckillConcurrentSafety() {Product product = new Product(iPhone 15, 100);SeckillService service = new SeckillService(product);// 启动 500 个用户抢购 100 件商品service.startSeckill(500);// 断言:成功数应为 100,剩余库存应为 0assertEquals(100, product.getStock() + 100); // 简化断言,实际应检查 successCount// 更严格的断言需要暴露 successCount,这里省略assertTrue(product.getStock() == 0, 库存应为0);}
}执行结果分析
运行测试后,预期输出如下:
秒杀开始,初始库存:100
秒杀结束,耗时:12ms
成功抢购数:100
剩余库存:0
数据一致性校验通过性能对比:
如果使用 synchronized 块保护扣减逻辑,在 500 线程竞争下,耗时通常会上升到 50-100ms 甚至更高,具体取决于 CPU 核心数。而 AtomicInteger 的 CAS 操作在硬件层面支持,性能优势明显。
避坑指南:ABA 问题: 虽然库存扣减是单调递减,不存在 ABA 问题,但在其他场景(如栈操作)中需注意。若需防范,可使用 AtomicStampedReference。
自旋耗时: 在高竞争下,CAS 失败会导致线程自旋,消耗 CPU。如果竞争激烈,可考虑分段锁或队列削峰。
内存可见性: AtomicInteger 内部使用 volatile 修饰变量,保证了线程间的可见性。不要手动将 int 改为 volatile 就认为线程安全,volatile 不保证原子性,1+1 操作仍是非原子的。优化扩展
1. 引入 Redis 分布式锁
单机内存方案无法应对集群部署。生产环境中,通常使用 Redis 的 DECR 命令或 Lua 脚本保证原子性。
Lua 脚本示例:
local stock = redis.call('get', KEYS[1])
if (tonumber(stock) 0) thenredis.call('decr', KEYS[1])return 1
elsereturn 0
end将 Java 端的 AtomicInteger 替换为 Redis 调用,即可实现分布式秒杀。
2. 异步订单处理
抢购成功后,不要同步创建订单,而是将用户 ID 放入消息队列(如 Kafka、RabbitMQ),由消费者异步生成订单。这样可以解耦抢购与订单创建,提升吞吐量。
3. 防刷机制
增加 IP 限流、验证码校验、用户身份校验等前置检查,减少无效请求到达核心扣减逻辑。
小结
通过这个项目,我们从零搭建了一个基于 AtomicInteger 的秒杀系统。核心在于理解 CAS 原理及其在 Java 中的实现方式。java.util.concurrent.atomic 包提供了丰富的原子类,是解决并发编程难题的利器。
在实际工作中,不要盲目使用锁,优先考虑无锁或细粒度锁方案。同时,务必进行压力测试,验证数据一致性。官方源码仓库 OpenJDK 中 AtomicInteger 的实现仅几行代码,但背后涉及 JVM 指令集优化,值得深入研读。
这个知识点你面试被问过吗?留言说说