线程池应用

黑色的灵眸大约 5 分钟工作常见问题总结Java线程池并发

线程池应用

线程池是工作中非常常见的并发工具,主要用于复用线程、控制并发数量、隔离不同类型的任务,避免请求量突然增大时无限创建线程导致系统资源耗尽。

为什么要使用线程池

直接创建线程虽然简单,但在业务系统中有几个明显问题:

  • 线程创建和销毁有成本,高频创建会浪费资源。
  • 线程数量不受控制,容易把 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 密集型任务:

线程数可以适当大一些,因为线程经常在等待网络、磁盘或数据库响应

但无论哪种任务,都要通过压测和监控验证,不能只靠公式。

推荐实践

  • 线程池必须命名,方便日志和监控定位。
  • 队列必须有界,避免任务无限堆积。
  • 拒绝策略不能忽略,至少要记录日志和监控。
  • 不同业务场景尽量拆分线程池。
  • 异步任务内部要捕获异常。
  • 线程池参数上线后要持续观察活跃线程数、队列长度、拒绝次数和任务耗时。

小结

线程池不是简单地把任务丢到后台执行,而是对并发资源做约束和治理。工作中使用线程池时,重点不是把线程数调大,而是让任务数量、执行速度、失败处理和系统承载能力处在可控范围内。

Loading...