一次多线程改造实践:基于ExecutorService + CompletionService的并发处理优化
📄 背景与业务场景
在企业业务系统中,我们经常需要对一批独立任务进行重复性的处理,例如:
- 为多个合同生成应收账单
- 对多个客户批量发送通知
- 针对多租户并行初始化资源
这类任务具备天然的并行性,即任务之间互不影响、处理逻辑相同,完全可以使用多线程优化执行效率。
本文以“为多个合同生成应收单”为例,记录一次从串行到并发的多线程改造实践,并重点介绍 ExecutorService + CompletionService 的使用方式。
📆 需求分析
原始功能需求:
- 输入为若干合同ID(可为全部合同或指定ID集合)
- 对每一个合同执行账单生成逻辑
- 聚合返回所有生成成功的应收单ID列表
- 该接口为前端“提交并审批”流程的一部分,必须同步返回处理结果
技术限制与期望:
- 合同数量多时串行处理耗时较长,需提升处理效率
- 合同任务之间互不依赖,可并行
- 需要对失败任务记录日志
- 并发线程数应可控,防止系统资源占满
📈 原始串行代码(伪代码)
List<Long> resultIds = new ArrayList<>();
for (Long contractId : contractIds) {
Contract contract = contractService.getContract(contractId);
if (contract == null) throw new RuntimeException("Contract not found");
Map<Long, Bill> bills = billService.generate(contract);
for (Bill bill : bills.values()) {
Long billId = billRepository.save(bill);
resultIds.add(billId);
}
}
return resultIds;
问题:
- 完全串行,处理效率低
- 合同处理耗时不均,无法优化瓶颈
🛠️ 改造方案:使用线程池并发处理合同任务
1. 技术选型
ThreadPoolExecutor:手动配置线程池参数,生产环境推荐使用CompletionService:结合线程池管理任务提交 + 异步结果获取,支持按完成顺序返回结果,便于聚合处理
2. 改造目标
- 将每一个合同处理逻辑封装为
Callable<List<Long>> - 提交至线程池统一调度执行
- 主线程收集
Future返回结果并聚合 - 控制最大并发线程数,保证系统安全
3. 自动创建线程池的缺点
使用 Executors 工具类创建线程池存在以下风险:
| 方法 | 缺点 | 风险 |
|---|---|---|
newFixedThreadPool | 无界队列 | 任务堆积导致 OOM |
newCachedThreadPool | 无界线程数+短存活时间 | 线程爆炸导致 OOM |
newSingleThreadExecutor | 无界队列+单线程 | 任务堆积导致 OOM |
核心问题:无法自定义队列容量、拒绝策略和线程工厂,缺乏资源控制能力。
📃 改造后关键代码(手动创建线程池)
// 根据合同数量动态设定核心线程数(最多10个线程)
int threadCount = Math.min(10, contractIds.size());
// 手动创建线程池,更可控,推荐用于生产环境
ExecutorService executorService = new ThreadPoolExecutor(
threadCount, // corePoolSize:核心线程数
threadCount, // maximumPoolSize:最大线程数
60L, // keepAliveTime:空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 有界工作队列(防止OOM)
new ThreadFactory() { // 自定义线程工厂
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "receivable-bill-thread-" + count.getAndIncrement());
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
CompletionService<List<Long>> completionService = new ExecutorCompletionService<>(executorService);
// 提交任务
for (Long contractId : contractIds) {
completionService.submit(() -> {
Contract contract = contractService.getContract(contractId);
if (contract == null) throw new RuntimeException("Contract missing");
Map<Long, Bill> billMap = billService.generate(contract);
List<Long> billIds = new ArrayList<>();
for (Bill bill : billMap.values()) {
Long billId = billRepository.save(bill);
billIds.add(billId);
}
return billIds;
});
}
// 收集结果
List<Long> allBillIds = new ArrayList<>();
for (int i = 0; i < contractIds.size(); i++) {
try {
Future<List<Long>> future = completionService.take(); // 阻塞直到有结果
allBillIds.addAll(future.get());
} catch (Exception e) {
log.error("Task failed", e);
// 可选:记录失败ID、重试等
}
}
executor.shutdown();
📖 线程池核心参数详解
ThreadPoolExecutor 构造函数参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
| 参数 | 定义与意义 | 配置建议 |
|---|---|---|
corePoolSize | 核心线程数:线程池长期维持的线程数量,即使空闲也不会被回收 | 根据 CPU 核心数或业务并发量设置(如 CPU核心数 * 2) |
maximumPoolSize | 最大线程数:线程池允许创建的最大线程数 | 核心线程数的 1~2 倍,避免线程上下文切换过多 |
keepAliveTime | 空闲线程存活时间:非核心线程空闲后的最大存活时间 | 60秒(默认值),根据任务执行周期调整 |
unit | 时间单位 | 通常使用 TimeUnit.SECONDS |
workQueue | 工作队列:存放等待执行的任务 | 必须使用有界队列(如 LinkedBlockingQueue(100))防止 OOM |
threadFactory | 线程工厂:自定义线程创建逻辑 | 设置有意义的线程名(如 task-thread-%d)便于问题排查 |
handler | 拒绝策略:任务超出处理能力时的处理方式 | 推荐 CallerRunsPolicy(调用者线程执行,降低任务丢失风险) |
线程池工作原理
- 提交任务时,优先创建核心线程执行
- 核心线程满时,任务进入工作队列等待
- 队列满时,创建非核心线程执行
- 总线程数达最大线程数且队列满时,触发拒绝策略
📖 CompletionService 原理解析
CompletionService
- 接口:
java.util.concurrent.CompletionService - 实现类:
ExecutorCompletionService - 优点:按任务完成顺序获取结果,而非提交顺序
流程:
submit(Callable):提交任务到线程池take()/poll():获取已完成任务的Futureget():获取具体结果或异常
这种模式避免了遍历所有 Future 检查状态,提高聚合效率
🎡 使用建议与扩展
✅ 使用建议
- 并发任务需满足线程安全、无共享状态
- 线程池应设置合理核心线程数、队列大小与拒绝策略
- 对于大量任务建议配合限流器如
Semaphore
📄 场景拓展
- 批量导入 / 导出数据
- 大文件分段处理
- 对多个接口异步调用并合并结果(如聚合查询)
📝 总结
通过使用 ExecutorService + CompletionService 对串行任务进行并发改造,能够在保证业务正确性的前提下大幅提升处理效率,尤其适合独立、重复、大量的任务场景。
本次实践中,改造后的实现满足了:
- 前端同步获取结果的业务需求
- 可控的并发处理与资源使用
- 良好的异常处理与日志记录
是一种在生产环境下非常推荐的并发模式。