线程池问题最容易伪装成“偶发超时”

很多系统里的异步能力不是一次设计出来的,而是慢慢叠上去的:

1、先有一个公共线程池

2、后面补批量任务

3、再加补偿、消息处理、外部调用

4、最后所有异步任务都往里面塞

这时线上最常见的表现不是线程池报错,而是接口慢、任务堆、偶发超时、日志里还看不出来是谁把池子打满了。

真正该先拆的,是任务模型,不是线程数

我现在更倾向按职责拆池,而不是按“项目里有异步”拆池。

比较实用的拆法通常是:

1、规则计算线程池

2、批量查询线程池

3、补偿任务线程池

4、消息消费后处理线程池

5、外部调用线程池

因为这几类任务的资源模型完全不同:

1、有的偏 CPU

2、有的偏 IO

3、有的容忍堆积

4、有的必须及时完成

如果混在一个池子里,出了问题基本只能看到“这个池满了”,但看不到是谁干的。

一套能进生产的线程池配置,至少要把边界定清楚

1
2
3
4
5
6
7
8
9
10
11
@Bean("asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("asyncExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

这里我更关心的不是参数具体值,而是它们表达的边界:

1、正常流量靠核心线程吃

2、突发流量先排队还是先扩容

3、打满以后是回压、丢弃还是报错

4、线程名能不能一眼看出归属

参数不是拍脑袋定的,应该对应任务语义。

拒绝策略本质上是业务策略,不是技术细节

很多线程池问题最后不是线程数不够,而是拒绝策略和业务语义不匹配。

1
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

比如:

1、任务不能丢,但允许主线程回压,可以考虑 CallerRunsPolicy

2、任务允许放弃,且不能拖垮主链路,才考虑 DiscardPolicy

如果一个必须执行的补偿任务配了丢弃策略,线上出问题时通常会非常隐蔽。

批量任务别直接一把梭进线程池

批量任务最容易犯的错,就是几千条任务直接丢进池子里,剩下全交给线程池兜。

我更愿意先做批次拆分,再把每批交给线程池:

1
2
3
4
5
6
7
8
9
10
11
CountDownLatch latch = new CountDownLatch(batchList.size());
for (List<Task> batch : batchList) {
executor.execute(() -> {
try {
handleBatch(batch);
} finally {
latch.countDown();
}
});
}
latch.await();

这样做至少有三个好处:

1、单次压入线程池的任务数可控

2、每批失败和成功更容易统计

3、收口点更明确,后续能补超时和异常统计

线程池最少要补哪些监控

如果线程池已经进生产,我现在至少会看:

1、活跃线程数

2、队列长度

3、任务拒绝次数

4、平均执行耗时

5、最大执行耗时

6、异常数量

没有这些指标时,很多线程池问题只能等到接口超时、补偿积压以后才暴露。

我更容易踩到的几个坑

1、不同类型任务共用一个池

最后谁都跑不快。

2、队列无限大

问题没有消失,只是从线程数转移到延迟和内存。

3、线程池里再起线程池

外层任务已经异步,内层又自己开池,资源模型很快就会乱。

4、任务本身时间不可控

慢 SQL、远程调用、重 CPU 逻辑都塞进一个任务里,参数调得再漂亮也没用。

如果线程池已经出问题,我一般怎么收

我自己的顺序通常是:

1、先按职责拆池

2、再补线程池监控

3、再改任务批次和超时控制

4、最后才微调核心线程数、最大线程数、队列长度

因为线程池问题大多数时候不是参数问题,而是任务边界问题。

小结

线程池治理不是把数字调大,而是把任务模型收清楚。

职责拆池决定资源隔离,拒绝策略决定流量打满时的业务语义,批次拆分决定线程池怎么被使用,监控决定你能不能在问题扩大前发现它。把这几层收住,异步链路才会稳。