JUC 在实际业务场景的落地实践
一、电商秒杀场景(核心:高并发库存扣减 + 限流)
痛点:
- 秒杀瞬间 QPS 达 10 万 +,库存扣减需线程安全且无阻塞;
- 需限制并发请求数,避免系统被打垮;
- 防止超卖、库存负数。
选型:
| 需求点 | 选型组件 | 原因 |
|---|---|---|
| 高并发库存计数 | LongAdder | 分段 CAS 累加,高并发下吞吐量比 AtomicLong 高 10 倍以上,避免计数瓶颈 |
| 接口限流(防冲垮) | Semaphore | 控制同时进入秒杀的请求数,超出则直接拒绝 |
| 异步处理订单 | ThreadPoolExecutor | 库存扣减成功后,异步提交订单处理任务,提升响应速度 |
流程:
获取失败/超时 获取中断(InterruptedException) 获取成功 商品不存在 商品存在 库存 ≤ 0 库存 > 0 扣减失败 扣减成功 用户发起秒杀请求
seckill(goodsId, userId) 尝试获取限流许可
Semaphore.tryAcquire(50ms) 返回:活动太火爆,请稍后重试 线程中断,返回:请求被中断 校验商品是否存在
stockMap.get(goodsId) 返回:商品不存在 获取当前库存
stock.sum() 返回:没有库存了 CAS 原子扣减库存
stock.compareAndSet(当前库存, 库存-1) 返回:库存被抢光了,手慢无 异步提交订单处理任务
orderExecutor.submit() 生成秒杀订单
createSeckillOrder() 发送秒杀成功短信
sendSeckillSuccessMsg() 返回:秒杀成功,订单正在生成 释放限流许可
Semaphore.release() 请求结束 订单业务完成
入库/扣积分/锁库存 短信发送完成
代码:
java
@Component
public class SeckillService {
// 秒杀商品库存(LongAdder 适配超高并发)
private final Map<Long, LongAdder> stockMap = new ConcurrentHashMap<>();
// 秒杀限流信号量(最多 2000 个并发请求)
private final Semaphore seckillSemaphore = new Semaphore(2000);
// 订单处理线程池(手动创建,避免 OOM)
private final ExecutorService orderExecutor = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10000),
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "seckill-order-" + count.getAndIncrement());
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行,避免任务丢失
);
// 初始化秒杀商品库存
@PostConstruct
public void initSeckillStock() {
LongAdder phoneStock = new LongAdder();
phoneStock.add(1000); // 秒杀手机库存 1000 台
stockMap.put(1001L, phoneStock); // 商品 ID:1001
}
// 秒杀核心接口
public Result<String> seckill(Long goodsId, Long userId) {
// 1. 尝试获取限流许可(超时 50ms 则拒绝)
try {
if (!seckillSemaphore.tryAcquire(50, TimeUnit.MILLISECONDS)) {
return Result.fail("活动太火爆,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("请求被中断");
}
try {
// 2. 校验商品是否存在
LongAdder stock = stockMap.get(goodsId);
if (stock == null) {
return Result.fail("商品不存在");
}
// 3. 原子扣减库存(sumThenGet:先求和再减 1,避免并发问题)
long currentStock = stock.sum();
if (currentStock <= 0) {
return Result.fail("没有库存了");
}
// CAS 思想:确保扣减时库存未被修改
boolean success = stock.compareAndSet(currentStock, currentStock - 1);
if (!success) {
return Result.fail("库存被抢光了,手慢无");
}
// 4. 异步处理订单(扣减成功后,异步生成订单、扣减积分等)
orderExecutor.submit(() -> {
createSeckillOrder(goodsId, userId); // 生成秒杀订单
sendSeckillSuccessMsg(userId); // 发送秒杀成功短信
});
return Result.success("秒杀成功,订单正在生成");
} finally {
// 5. 释放限流许可(必须在 finally 中释放)
seckillSemaphore.release();
}
}
// 生成秒杀订单(业务逻辑)
private void createSeckillOrder(Long goodsId, Long userId) {
// 订单入库、扣减用户积分、锁定库存等逻辑
log.info("生成秒杀订单:商品{},用户{}", goodsId, userId);
}
// 发送秒杀成功短信(业务逻辑)
private void sendSeckillSuccessMsg(Long userId) {
log.info("给用户{}发送秒杀成功短信", userId);
}
}
注意:
- Semaphore 许可必须释放:无论秒杀成功 / 失败,都要在 finally 中 release,否则许可会被耗尽,后续请求全部被拒;
- 库存扣减需原子性:不能先 get 再 set(会导致超卖),必须用 LongAdder 的 CAS 方法;
- 线程池拒绝策略选型:秒杀场景用 CallerRunsPolicy,让主线程执行任务,避免订单丢失(比丢弃策略更友好)。
二、支付回调场景(核心:异步处理 + 顺序执行)
痛点:
- 支付平台(微信 / 支付宝)会多次推送回调通知,需保证订单状态更新的顺序性;
- 回调处理需异步执行,避免阻塞支付平台响应;
- 防止重复处理同一回调(如多次推送同一支付成功通知)。
选型:
| 需求点 | 选型组件 | 原因 |
|---|---|---|
| 异步处理回调 | ThreadPoolExecutor | 异步处理回调逻辑,快速响应支付平台(要求 1 秒内返回) |
| 订单状态顺序更新 | newSingleThreadExecutor | 单个订单的回调用单线程串行执行,避免并发修改订单状态(如 "支付中" 和 "支付成功" 并发) |
| 防止重复处理 | ConcurrentHashMap | 缓存已处理的回调单号,原子判断是否已处理 |
流程:
已处理(返回非null) 未处理(返回null) 是 否 是 否 支付平台推送回调
POST /pay/callback Controller 接收回调参数
handlePayCallback(callbackDTO) 异步提交核心处理任务
coreExecutor.submit(processCallback) 立即返回 SUCCESS
快速响应支付平台 请求响应完成 处理回调核心逻辑
processCallback(callbackDTO) 获取回调单号/订单ID/支付状态 防重校验
processedCallbackMap.putIfAbsent(callbackNo) 日志记录:回调单号已处理,忽略 获取订单专属单线程池
orderCallbackExecutorMap.computeIfAbsent 提交订单状态更新任务
orderExecutor.submit() 更新订单状态
updateOrderStatus(orderId, tradeStatus) 支付状态是否为 SUCCESS? 触发支付成功业务
triggerOrderSuccessBiz(orderId) 订单状态更新完成(无后续业务) 订单业务处理完成 执行过程抛出异常 日志记录:处理订单回调失败 移除已处理标记
processedCallbackMap.remove(callbackNo)(允许重试) 异常处理完成 订单是否完成? 销毁订单专属线程池
destroyOrderExecutor(orderId) 线程池保留,等待后续回调 移除并关闭线程池,释放资源 回调处理全流程结束
代码:
java
@Component
public class PayCallbackService {
// 已处理的回调单号(防重)
private final ConcurrentHashMap<String, Boolean> processedCallbackMap = new ConcurrentHashMap<>();
// 订单回调处理线程池(每个订单一个单线程池,保证顺序)
private final ConcurrentHashMap<Long, ExecutorService> orderCallbackExecutorMap = new ConcurrentHashMap<>();
// 核心线程池(用于创建订单专属线程池)
private final ExecutorService coreExecutor = Executors.newFixedThreadPool(10);
// 支付回调入口
@PostMapping("/pay/callback")
public String handlePayCallback(@RequestBody PayCallbackDTO callbackDTO) {
// 1. 快速响应支付平台(异步处理核心逻辑)
coreExecutor.submit(() -> processCallback(callbackDTO));
return "SUCCESS"; // 必须快速返回,否则支付平台会重试
}
// 处理回调核心逻辑
private void processCallback(PayCallbackDTO callbackDTO) {
String callbackNo = callbackDTO.getCallbackNo(); // 回调唯一单号
Long orderId = callbackDTO.getOrderId(); // 订单 ID
String tradeStatus = callbackDTO.getTradeStatus(); // 支付状态:SUCCESS/FAIL
// 2. 防重处理(原子操作,避免重复处理)
if (processedCallbackMap.putIfAbsent(callbackNo, Boolean.TRUE) != null) {
log.info("回调单号{}已处理,忽略", callbackNo);
return;
}
// 3. 获取订单专属单线程池(保证同一订单的回调顺序执行)
ExecutorService orderExecutor = orderCallbackExecutorMap.computeIfAbsent(orderId, k ->
Executors.newSingleThreadExecutor(r -> new Thread(r, "pay-callback-" + k)));
// 4. 提交订单状态更新任务(串行执行)
orderExecutor.submit(() -> {
try {
// 更新订单状态(串行执行,避免并发问题)
updateOrderStatus(orderId, tradeStatus);
// 触发后续业务(如发货、积分发放)
if ("SUCCESS".equals(tradeStatus)) {
triggerOrderSuccessBiz(orderId);
}
} catch (Exception e) {
log.error("处理订单{}回调失败", orderId, e);
// 异常时移除已处理标记,允许重试
processedCallbackMap.remove(callbackNo);
}
});
}
// 更新订单状态(业务逻辑)
private void updateOrderStatus(Long orderId, String tradeStatus) {
log.info("更新订单{}状态为{}", orderId, tradeStatus);
// 订单状态入库逻辑(如:待支付→支付成功)
}
// 订单支付成功后触发的业务(业务逻辑)
private void triggerOrderSuccessBiz(Long orderId) {
log.info("订单{}支付成功,触发发货、积分发放", orderId);
// 发货通知、用户积分增加、商家结算等逻辑
}
// 销毁订单线程池(订单完成后清理资源)
public void destroyOrderExecutor(Long orderId) {
ExecutorService executor = orderCallbackExecutorMap.remove(orderId);
if (executor != null) {
executor.shutdown();
}
}
}
注意:
- 单线程池需清理:订单完成后要销毁专属线程池,否则线程数会随订单量增长导致资源耗尽;
- 防重标记需容错:处理失败时要移除已处理标记,允许支付平台重试;
- 快速响应支付平台:回调接口必须异步处理,1 秒内返回 SUCCESS,否则支付平台会多次重试。
三、物流轨迹同步场景(核心:定时任务 + 阻塞队列)
痛点:
- 需定时从物流平台拉取轨迹数据(如每 5 分钟拉取一次);
- 拉取的数据量可能很大,需异步处理入库;
- 避免拉取任务和处理任务的耦合,防止处理慢导致拉取阻塞。
选型:
| 需求点 | 选型组件 | 原因 |
|---|---|---|
| 定时拉取轨迹 | ScheduledThreadPoolExecutor | 支持周期性任务,多线程执行,避免单线程阻塞 |
| 轨迹数据缓冲 | LinkedBlockingQueue | 阻塞队列缓冲拉取的数据,解耦拉取和处理流程 |
| 异步处理入库 | ThreadPoolExecutor | 多线程处理轨迹入库,提升处理效率 |
流程:
是 否 是 否 是 否 是 否 服务初始化
@PostConstruct 启动定时拉取任务
pullExecutor.scheduleAtFixedRate 启动5个轨迹处理消费者
processExecutor.submit(processTrack) 延迟1分钟启动,每5分钟执行一次
pullLogisticsTrack() 调用物流平台API拉取轨迹数据
logisticsApi.pullTrack() 拉取是否异常? 日志记录:拉取物流轨迹失败 日志记录:拉取到N条轨迹数据 遍历轨迹数据,放入阻塞队列
trackQueue.put(track) 队列是否已满? 阻塞等待,直到队列有空闲位置 数据成功入队 拉取任务本轮结束,等待下一轮调度 消费者线程循环执行
!Thread.currentThread().isInterrupted() 从阻塞队列取数据
trackQueue.take() 队列是否为空? 阻塞等待,直到队列有数据 获取轨迹数据LogisticsTrackDTO 轨迹数据入库
saveTrackToDb(track) 更新订单物流状态
updateOrderTrackStatus() 处理是否异常? 日志记录:处理物流轨迹失败 处理完成,回到循环 线程被中断(InterruptedException) 标记线程中断,退出循环 服务销毁
@PreDestroy 关闭拉取线程池
pullExecutor.shutdown() 关闭处理线程池
processExecutor.shutdown() 消费者线程终止 拉取任务停止调度 物流轨迹同步服务停止
代码:
java
@Component
public class LogisticsTrackService {
// 轨迹数据缓冲队列(容量 10000,避免内存溢出)
private final BlockingQueue<LogisticsTrackDTO> trackQueue = new LinkedBlockingQueue<>(10000);
// 定时拉取线程池(核心线程数 2)
private final ScheduledExecutorService pullExecutor = Executors.newScheduledThreadPool(2);
// 轨迹处理线程池(核心线程数 5)
private final ExecutorService processExecutor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5000),
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "logistics-process-" + count.getAndIncrement());
}
},
new ThreadPoolExecutor.DiscardOldestPolicy() // 丢弃最老任务,保证最新数据
);
// 启动定时拉取任务
@PostConstruct
public void startPullTask() {
// 每 5 分钟拉取一次物流轨迹(延迟 1 分钟启动)
pullExecutor.scheduleAtFixedRate(this::pullLogisticsTrack,
1, 5, TimeUnit.MINUTES);
// 启动轨迹处理线程(5 个消费者)
for (int i = 0; i < 5; i++) {
processExecutor.submit(this::processTrack);
}
}
// 拉取物流轨迹(生产者)
private void pullLogisticsTrack() {
try {
// 调用物流平台 API 拉取轨迹数据
List<LogisticsTrackDTO> trackList = logisticsApi.pullTrack();
log.info("拉取到{}条物流轨迹数据", trackList.size());
// 将数据放入阻塞队列(队列满则阻塞,避免数据丢失)
for (LogisticsTrackDTO track : trackList) {
trackQueue.put(track);
}
} catch (Exception e) {
log.error("拉取物流轨迹失败", e);
}
}
// 处理物流轨迹(消费者)
private void processTrack() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 从队列取数据(队列空则阻塞)
LogisticsTrackDTO track = trackQueue.take();
// 轨迹数据入库
saveTrackToDb(track);
// 更新订单物流状态
updateOrderTrackStatus(track.getOrderId(), track);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("处理物流轨迹失败", e);
}
}
}
// 轨迹入库(业务逻辑)
private void saveTrackToDb(LogisticsTrackDTO track) {
log.info("轨迹入库:订单{},轨迹{}", track.getOrderId(), track.getTrackDesc());
// 轨迹数据入库逻辑
}
// 更新订单物流状态(业务逻辑)
private void updateOrderTrackStatus(Long orderId, LogisticsTrackDTO track) {
log.info("更新订单{}物流状态为{}", orderId, track.getTrackStatus());
// 订单物流状态更新逻辑
}
// 关闭线程池
@PreDestroy
public void shutdown() {
pullExecutor.shutdown();
processExecutor.shutdown();
}
}
注意:
- 阻塞队列容量要限制:避免无界队列导致内存溢出,根据业务量设置合理容量;
- 消费者线程需异常处理:捕获异常避免消费者线程挂掉,保证消费能力;
- 定时任务异常隔离:拉取任务失败时,不影响下一次执行(ScheduledThreadPoolExecutor 会自动重试)。
四、后台批量导出场景(核心:线程池 + 进度统计)
痛点:
- 后台导出大量数据(如 100 万条订单),需分批处理,避免内存溢出;
- 需实时返回导出进度给前端;
- 导出任务需异步执行,不阻塞前端请求。
选型:
| 需求点 | 选型组件 | 原因 |
|---|---|---|
| 异步导出任务 | ThreadPoolExecutor | 分批处理导出任务,控制并发数,避免数据库压力过大 |
| 导出进度统计 | AtomicLong | 原子统计已处理条数,实时返回进度 |
| 任务结果存储 | ConcurrentHashMap | 缓存导出任务的状态和进度,供前端查询 |
流程:
是 否 前端发起导出请求
调用 startExport(param) 生成唯一任务ID(UUID) 统计导出总条数
countOrderTotal(param) 初始化导出进度对象
ExportProgress(status=PROCESSING) 缓存进度到ConcurrentHashMap
exportProgressMap.put(taskId, progress) 异步提交导出任务
exportExecutor.submit() 立即返回任务ID给前端
前端用于查询进度 前端轮询查询进度
调用 queryExportProgress(taskId) 从缓存获取进度
exportProgressMap.get(taskId) 返回进度信息给前端
包含status/processedCount/progressRate 执行导出核心逻辑
exportOrderData(taskId, param, progress) 计算总分批数
totalPage = ceil(总条数/1000) 循环分批处理
pageNum 从1到totalPage 分页查询订单数据
queryOrderByPage(pageNum, 1000) 写入订单数据到文件
writeOrderToFile(taskId, orderList) 原子更新已处理条数
processedCount.addAndGet(条数) 计算进度百分比
progressRate = 已处理/总条数*100 所有批次处理完成? 更新进度状态为SUCCESS
progress.setStatus(SUCCESS) 执行过程抛出异常 日志记录:导出失败(任务ID+异常信息) 更新进度状态为FAIL
设置errorMsg 导出任务完成 线程池队列满/线程数达上限 触发AbortPolicy拒绝策略 返回导出繁忙提示给前端 请求结束
代码:
java
@Component
public class OrderExportService {
// 导出任务状态缓存(key:任务 ID,value:导出进度)
private final ConcurrentHashMap<String, ExportProgress> exportProgressMap = new ConcurrentHashMap<>();
// 导出线程池(核心线程数 3,避免数据库连接耗尽)
private final ExecutorService exportExecutor = new ThreadPoolExecutor(
3, 5, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "order-export-" + count.getAndIncrement());
}
},
new ThreadPoolExecutor.AbortPolicy() // 拒绝新任务,返回导出繁忙
);
// 启动导出任务
public String startExport(ExportParam param) {
// 生成唯一任务 ID
String taskId = UUID.randomUUID().toString();
// 初始化导出进度
ExportProgress progress = new ExportProgress();
progress.setTaskId(taskId);
progress.setStatus("PROCESSING");
progress.setTotalCount(countOrderTotal(param)); // 统计总条数
exportProgressMap.put(taskId, progress);
// 异步执行导出任务
exportExecutor.submit(() -> {
try {
exportOrderData(taskId, param, progress);
progress.setStatus("SUCCESS");
} catch (Exception e) {
log.error("导出订单失败,任务ID:{}", taskId, e);
progress.setStatus("FAIL");
progress.setErrorMsg(e.getMessage());
}
});
return taskId;
}
// 导出订单数据(分批处理)
private void exportOrderData(String taskId, ExportParam param, ExportProgress progress) {
int pageSize = 1000; // 每批处理 1000 条
int totalPage = (int) Math.ceil((double) progress.getTotalCount() / pageSize);
// 分批查询并写入文件
for (int pageNum = 1; pageNum <= totalPage; pageNum++) {
// 查询当前批次数据
List<OrderDTO> orderList = queryOrderByPage(param, pageNum, pageSize);
// 写入文件(如 CSV/Excel)
writeOrderToFile(taskId, orderList);
// 更新进度(原子操作)
progress.getProcessedCount().addAndGet(orderList.size());
// 计算进度百分比
progress.setProgressRate((double) progress.getProcessedCount().get() / progress.getTotalCount() * 100);
}
}
// 查询导出进度(供前端调用)
public ExportProgress queryExportProgress(String taskId) {
return exportProgressMap.get(taskId);
}
// 统计订单总数(业务逻辑)
private long countOrderTotal(ExportParam param) {
// 数据库 count 统计逻辑
return 1000000L; // 模拟 100 万条
}
// 分页查询订单(业务逻辑)
private List<OrderDTO> queryOrderByPage(ExportParam param, int pageNum, int pageSize) {
// 数据库分页查询逻辑
return new ArrayList<>(); // 模拟数据
}
// 写入订单到文件(业务逻辑)
private void writeOrderToFile(String taskId, List<OrderDTO> orderList) {
// 文件写入逻辑(如写入服务器临时目录)
log.info("任务{}写入{}条订单数据", taskId, orderList.size());
}
// 导出进度实体
@Data
public static class ExportProgress {
private String taskId;
private String status; // PROCESSING/SUCCESS/FAIL
private long totalCount; // 总条数
private AtomicLong processedCount = new AtomicLong(0); // 已处理条数
private double progressRate; // 进度百分比
private String errorMsg; // 错误信息
}
}
注意:
- 分批处理数据:避免一次性查询 100 万条数据导致内存溢出,每批 1000-5000 条为宜;
- 进度统计原子化:用 AtomicLong 统计已处理条数,避免并发更新进度导致数据错误;
- 线程池核心数适配数据库连接:导出任务依赖数据库查询,核心线程数不能超过数据库连接池大小,避免连接耗尽。
五、避坑小指南
- 线程池不要用 Executors 一键创建:
- newFixedThreadPool/newSingleThreadExecutor 用无界队列,任务堆积会导致 OOM;
- newCachedThreadPool 核心线程数 0,高并发下会创建大量线程导致 CPU 100%;
- 推荐手动创建 ThreadPoolExecutor,指定有界队列和合理的拒绝策略。
- 锁 / 许可必须手动释放:
- ReentrantLock 必须在 finally 中 unlock;
- Semaphore 必须在 finally 中 release;
- 否则会导致死锁 / 许可耗尽。
- 并发容器慎用迭代器:
- ConcurrentHashMap 迭代器是弱一致性的,不保证实时性;
- CopyOnWriteArrayList 迭代时不会抛 ConcurrentModificationException,但迭代的是快照数据。
- 避免线程池任务无限堆积:
- 有界队列 + 合理的拒绝策略(如 CallerRunsPolicy/DiscardOldestPolicy);
- 监控线程池的队列长度,超过阈值时告警。
- 异步任务需考虑异常处理:
- 线程池任务的异常不会主动抛出,需在任务内部捕获并记录日志;
- 用 CompletableFuture 时,需用 exceptionally 处理异常。
六、总结
JUC 在实际业务中的落地核心是:
先拆解业务痛点(并发量、读写比例、是否异步、是否需要顺序执行),再匹配组件的核心特性,最后做好资源管控和异常处理。
高并发计数 → LongAdder/Atomic 类;
限流 / 资源控制 → Semaphore;
异步任务 → 手动创建 ThreadPoolExecutor;
顺序执行 → newSingleThreadExecutor;
生产者消费者 → BlockingQueue;
进度统计 → AtomicLong/ConcurrentHashMap。