"线程池我不是早就会了吗?corePoolSize、maxPoolSize、queueSize 都能背下来!"
------ 真正出事故的时候你就知道,配置这仨数,坑多得跟高考数学题一样。
一、线上事故复盘:任务全卡死,日志一片寂静
几个月前有个定时任务服务,凌晨会并发处理上千个文件。按理说线程池能轻松抗住。
结果那天凌晨,监控报警:任务积压 5 万条,机器 CPU 却只有 3%!
去看线程 dump:
arduino
pool-1-thread-1 waiting on queue.take()
pool-1-thread-2 waiting on queue.take()
...
线程都在等任务,但任务明明在队列里!
当时线程池配置如下:
java
new ThreadPoolExecutor(
5,
10,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000),
new ThreadPoolExecutor.AbortPolicy()
);
看起来没毛病对吧?
实际结果是:拒绝策略从未生效、maxPoolSize 永远没机会触发。
二、真相:线程池参数不是你想的那样配的
要理解问题,得先知道 ThreadPoolExecutor 的任务提交流程。
markdown
任务提交 → 核心线程是否满?
↓ 否 → 新建核心线程
↓ 是 → 队列是否满?
↓ 否 → 放入队列等待
↓ 是 → 是否小于最大线程数?
↓ 是 → 创建非核心线程
↓ 否 → 拒绝策略触发
也就是说:
只要队列没满,线程池就不会创建非核心线程。
所以:
- 你的
corePoolSize = 5; - 队列能放
10000个任务; maxPoolSize = 10永远不会触发;- 线程永远就那 5 个在干活;
- 队列里的任务越堆越多,拒绝策略永远"假死"。
三、踩坑场景实录
| 场景 | 错误配置 | 结果 |
|---|---|---|
| 高频接口异步任务 | LinkedBlockingQueue<>(10000) |
队列太大 → 拒绝策略形同虚设 |
| IO密集型任务 | 核心线程过少(如5) | CPU空闲但任务堆积 |
| CPU密集型任务 | 核心线程过多(如50) | 上下文切换浪费CPU |
| 线程池共用 | 多个模块共用一个 pool | 某任务阻塞导致全局"死锁" |
四、正确配置姿势(我现在都这么配)
思路很简单:
"小队列 + 合理核心数 + 合理拒绝策略 "
而不是 "大队列 + 盲目扩大线程数"。
例如 CPU 密集型任务:
ini
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cores + 1,
cores + 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
IO 密集型任务:
ini
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cores * 2,
cores * 4,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);
关键思想:
- 宁可拒绝,也不要堆积。
- 拒绝意味着"系统过载",堆积意味着"慢性自杀"。
五、拒绝策略的"假死"与自定义方案
内置的 4 种拒绝策略:
AbortPolicy:直接抛异常(最安全)CallerRunsPolicy:调用方线程执行(可限流)DiscardPolicy:悄悄丢弃任务(最危险)DiscardOldestPolicy:丢最老的(仍可能乱序)
如果你想更智能一点,可以自定义:
typescript
new ThreadPoolExecutor.AbortPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.warn("任务被拒绝,当前队列:{}", e.getQueue().size());
// 可以上报监控 / 发报警
}
};
六、监控才是救命稻草
别等到队列堆积了才发现问题。
我建议给线程池加实时监控,比如:
scss
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
monitor.scheduleAtFixedRate(() -> {
log.info("PoolSize={}, Active={}, QueueSize={}",
executor.getPoolSize(),
executor.getActiveCount(),
executor.getQueue().size());
}, 0, 5, TimeUnit.SECONDS);
这样你能第一时间看到线程数没涨、队列在爆。
🧠 七、总结(踩坑后记)
| 项目 | 错误思路 | 正确思路 |
|---|---|---|
| corePoolSize | 设太小 | 根据 CPU/I/O 特性动态计算 |
| queueCapacity | 设太大 | 保持小容量以触发拒绝策略 |
| maxPoolSize | 没触发 | 仅当队列满后才会启用 |
| 拒绝策略 | 默认 Abort | 建议自定义/限流处理 |
| 监控 | 没有 | 定期打印状态日志 |
最后一句话:
"线程池是救命的工具,用不好就变慢性毒药。"
✍️ 写在最后
如果你看到这里,不妨想想自己的系统里有多少个 newFixedThreadPool、多少个默认 LinkedBlockingQueue 没有限制大小。
你以为是"优化",其实是定时炸弹。