线程池应用
线程池应用
线程池是工作中非常常见的并发工具,主要用于复用线程、控制并发数量、隔离不同类型的任务,避免请求量突然增大时无限创建线程导致系统资源耗尽。
为什么要使用线程池
直接创建线程虽然简单,但在业务系统中有几个明显问题:
- 线程创建和销毁有成本,高频创建会浪费资源。
- 线程数量不受控制,容易把 CPU、内存或数据库连接打满。
- 任务执行缺少统一管理,不方便监控、降级和排查问题。
- 不同业务任务混在一起,某一类慢任务可能拖垮整个系统。
线程池的价值就是把线程资源管理起来,让任务提交、排队、执行、拒绝都有明确规则。
常见应用场景
异步处理非核心流程
例如用户下单成功后,主流程只负责保存订单和返回结果,短信通知、邮件通知、日志上报、埋点统计可以放到线程池中异步执行。
orderService.createOrder(orderRequest);
notifyExecutor.execute(() -> {
smsService.sendOrderSuccessMessage(userId);
emailService.sendOrderSuccessEmail(userId);
});
这类场景要注意:异步任务失败不能影响主流程,但需要记录日志或补偿。
批量任务并发处理
例如批量查询用户信息、批量处理文件、批量同步数据,可以用线程池提升处理效率。
List<CompletableFuture<Void>> futures = userIds.stream()
.map(userId -> CompletableFuture.runAsync(() -> {
userService.refreshUserCache(userId);
}, workerExecutor))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
这类场景要控制并发数量,不能因为批量数据太大导致下游接口或数据库压力过高。
定时任务拆分执行
定时任务中如果一次需要处理大量数据,可以先分页查询,再把每页数据提交到线程池处理。
for (List<Long> batchIds : batches) {
taskExecutor.execute(() -> {
syncService.syncBatch(batchIds);
});
}
需要注意定时任务不能无限提交任务,否则上一轮还没执行完,下一轮又开始提交,会造成任务堆积。
线程池核心参数
Java 中常用的是 ThreadPoolExecutor。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("biz-worker-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
核心参数说明:
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数,常驻线程数量 |
| maximumPoolSize | 最大线程数,队列满后最多可扩容到这个数量 |
| keepAliveTime | 非核心线程空闲多久后回收 |
| workQueue | 等待执行的任务队列 |
| threadFactory | 线程工厂,建议设置线程名前缀 |
| rejectedExecutionHandler | 拒绝策略,线程池和队列都满时如何处理 |
拒绝策略怎么选
常见拒绝策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛异常 | 希望快速暴露问题 |
| CallerRunsPolicy | 调用方线程自己执行 | 可以反压调用方,比较稳妥 |
| DiscardPolicy | 直接丢弃任务 | 允许丢弃的非核心任务 |
| DiscardOldestPolicy | 丢弃队列最旧任务 | 很少使用,容易造成不可控丢失 |
业务系统中更常见的是自定义拒绝策略,记录日志、打点监控,必要时触发告警。
队列不要无界
不建议使用无界队列,例如:
new LinkedBlockingQueue<>()
无界队列表面上不会拒绝任务,但任务提交速度超过消费速度时,队列会持续增长,最终可能导致内存溢出。
建议使用有界队列:
new LinkedBlockingQueue<>(1000)
队列大小需要结合业务峰值、任务耗时和机器资源评估,不能随便拍一个特别大的数。
线程池隔离
不同业务最好使用不同线程池,避免互相影响。
例如:
orderExecutor:订单相关异步任务notifyExecutor:短信、邮件、站内信通知syncExecutor:第三方数据同步reportExecutor:报表、导出、统计任务
如果所有任务共用一个线程池,某个慢接口或大批量任务可能占满线程,导致其他正常任务也无法执行。
常见问题
线程池满了怎么办
先看三个指标:
- 活跃线程数是否长期接近最大线程数
- 队列长度是否持续增长
- 任务执行耗时是否变长
处理方式通常有:
- 降低任务提交速度
- 优化单个任务耗时
- 调整核心线程数和队列大小
- 对非核心任务做降级或丢弃
- 拆分线程池,避免互相影响
异步任务异常为什么看不到
线程池中的异常如果没有捕获,可能只在子线程里打印,主流程感知不到。
建议在任务内部捕获异常并记录业务上下文:
executor.execute(() -> {
try {
syncService.syncData(id);
} catch (Exception e) {
log.error("sync data failed, id={}", id, e);
}
});
线程池参数怎么设置
没有通用固定答案,需要结合任务类型。
CPU 密集型任务:
线程数接近 CPU 核数
IO 密集型任务:
线程数可以适当大一些,因为线程经常在等待网络、磁盘或数据库响应
但无论哪种任务,都要通过压测和监控验证,不能只靠公式。
推荐实践
- 线程池必须命名,方便日志和监控定位。
- 队列必须有界,避免任务无限堆积。
- 拒绝策略不能忽略,至少要记录日志和监控。
- 不同业务场景尽量拆分线程池。
- 异步任务内部要捕获异常。
- 线程池参数上线后要持续观察活跃线程数、队列长度、拒绝次数和任务耗时。
小结
线程池不是简单地把任务丢到后台执行,而是对并发资源做约束和治理。工作中使用线程池时,重点不是把线程数调大,而是让任务数量、执行速度、失败处理和系统承载能力处在可控范围内。
