4. 线程池原理与调优:ThreadPoolExecutor核心参数、拒绝策略、线程池大小估算方法
线程池这东西,说白了就是「复用线程」。
我见过太多项目,每次来任务就 new Thread(),结果系统直接被打崩。你想想看,线程的创建和销毁是有代价的——上下文切换、内存分配、内核态切换,这些开销加起来,高并发下就是灾难。
所以 Java 1.5 开始提供了 ThreadPoolExecutor。今天我就带你把它彻底搞明白。
4.1 核心参数:七个参数,一个都不能少
ThreadPoolExecutor 的构造函数有七个参数。我习惯把它们分成三组来理解:
| 参数 | 含义 | 我的经验 |
|---|---|---|
| corePoolSize | 核心线程数 | 常驻线程,即使空闲也不回收 |
| maximumPoolSize | 最大线程数 | 线程池能容纳的线程上限 |
| keepAliveTime | 空闲线程存活时间 | 超过 corePoolSize 的线程空闲多久后被回收 |
| unit | 时间单位 | 一般用 TimeUnit.SECONDS |
| workQueue | 任务队列 | 存放等待执行的任务 |
| threadFactory | 线程工厂 | 给线程起名字,方便排查问题 |
| handler | 拒绝策略 | 队列满了、线程数达到上限时怎么办 |
核心流程:
- 提交任务 → 检查当前线程数是否小于 corePoolSize → 是则新建线程执行
- 否则 → 尝试放入 workQueue 等待
- 队列满了 → 检查是否小于 maximumPoolSize → 是则新建线程执行
- 否则 → 执行拒绝策略
嗯,这里要注意:corePoolSize 不是硬性下限。如果设置了 allowCoreThreadTimeOut(true),核心线程也会被回收。我一般只在需要动态缩容的场景才开这个开关。
4.2 拒绝策略:四种选择,各有千秋
当线程池「扛不住」了,就会触发拒绝策略。JDK 内置了四种:
- AbortPolicy(默认):直接抛 RejectedExecutionException。我个人觉得这策略太粗暴,线上环境慎用。
- CallerRunsPolicy:谁提交的任务,谁自己跑。说白了就是让调用线程去执行。我在项目中遇到过,这个策略能起到「反压」效果——调用方变慢了,自然就不会疯狂提交任务了。
- DiscardPolicy:悄悄丢弃任务,不抛异常。嗯,这个我基本不用,丢任务连个日志都没有,排查问题会疯掉。
- DiscardOldestPolicy:丢弃队列里最老的任务,然后重新提交当前任务。适合允许「过期任务」被丢弃的场景。
我的建议:线上环境优先考虑 CallerRunsPolicy。它不会丢任务,还能自动限流。我曾经在一个支付系统中用过,效果很好。
4.3 线程池大小估算方法:别再拍脑袋了
「线程池设多大?」这是面试高频题,也是实际项目里最容易踩坑的地方。
我见过有人直接写 200、500,问为什么,答「感觉差不多」。嗯,这不行。
正确的估算方法,要看任务是 CPU 密集型还是 IO 密集型:
- CPU 密集型:线程数 = CPU 核心数 + 1。为什么加 1?因为总有少量页面缺页、缓存未命中,多一个线程能利用这些「空档」。
- IO 密集型:线程数 = CPU 核心数 × (1 + 等待时间 / 计算时间)。这个公式来自《Java 并发编程实战》,我用了很多年,很靠谱。
举个例子:
// 假设 CPU 是 8 核
// 任务计算耗时 10ms,IO 等待耗时 90ms
// 那么:8 × (1 + 90/10) = 8 × 10 = 80
// 所以线程池大小建议设为 80
注意:这个公式只是起点。实际项目中,我建议先按公式算出一个值,然后做压测,观察 CPU 利用率、响应时间、吞吐量,再微调。我曾经在一个网关项目中,公式算出来是 64,压测后发现 48 效果更好——因为线程太多反而增加了上下文切换开销。
4.4 核心知识体系
下面这张图,是我自己总结的线程池调优全景图:
4.5 实战建议:我踩过的坑
最后分享几个我实际项目中踩过的坑:
- 队列选型要谨慎:LinkedBlockingQueue 如果不设置容量,默认是 Integer.MAX_VALUE。这意味着任务会无限堆积,线程数永远达不到 maxPoolSize。我曾经在一个消息处理系统中用了无界队列,结果内存直接爆了。
- 一定要给线程命名:用 threadFactory 给线程起个有意义的名字,比如「order-process-%d」。这样出问题时,jstack 一看就知道是哪个线程池在搞鬼。
- 监控不能少:线程池的 activeCount、queueSize、completedTaskCount 这些指标,建议都暴露到监控系统。我习惯用 Micrometer 或者直接写个定时任务打印日志。
- 动态调整:setCorePoolSize 和 setMaximumPoolSize 可以在运行时调用。我建议在管理后台留个接口,方便线上紧急调整。
一句话总结:线程池调优没有银弹。先按公式估算,再压测验证,最后持续监控。别偷懒,也别迷信某个固定值。
可可精品资料库