线程池概述与核心参数简介
说起线程池,我猜你第一反应就是那六个参数——corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。没错,它们就像调音台上的六个旋钮,拧对了,系统跑得飞起;拧错了,CPU 打满、内存溢出、任务堆积……这些坑我都踩过。
先聊聊线程池到底解决什么问题。说白了就是:复用线程,避免频繁创建销毁,控制并发度。你想想看,如果每个请求过来都 new Thread(),系统很快就崩了。我记得刚工作那会儿,项目里就有人这么干,结果线上每秒 2000 个请求,线程数直接冲到几万,然后 JVM 就挂了。后来改成线程池,问题瞬间解决。
核心思想:用有限的线程处理无限的任务队列。
线程池的工作流程
我习惯用一张图来理解线程池的运作——它像一条流水线,有固定工人、临时工、等待区,还有急事处理机制。下面是这张关系图:
这是我在生产环境里实际总结出来的关系图。每个参数都有自己的角色,一个调不好,整个池子就乱套。
六大核心参数详解
1. corePoolSize(核心线程数)
这个参数定义池子里常驻的线程数量。说白了,就是即使这些线程闲着,也不会被回收。我自己的经验是:核心线程数通常设成 CPU 核心数 + 1(IO 密集型可以更大)。比如一个 4 核的机器,我会从 5 开始试。
如果你的任务大多是 CPU 密集型,corePoolSize 别超过 CPU 核心数太多,否则线程切换的开销会吃掉性能。
2. maximumPoolSize(最大线程数)
这是线程池能创建的线程上限。当工作队列满之后,线程池会继续创建新线程,直到达到这个上限。我曾经在压测时犯过一个错误:maximumPoolSize 设得太大(比如 500),结果队列还没满,线程数就冲到了 300,把 CPU 跑满了。后来我严格控制上限,一般不超过 200,再配合合理的队列长度。
3. keepAliveTime(空闲线程存活时间)
非核心线程空闲多久后被回收。默认是 60 秒,但我习惯按业务忙闲程度来调。比如晚上流量低,可以设短一点(10 秒),白天流量高,设长一点(300 秒)。
4. workQueue(工作队列)
用来存放等待执行的任务。Java 提供了几种常用队列:
- SynchronousQueue:不存储任务,直接转给线程。要求 maximumPoolSize 足够大,不然容易拒绝。
- LinkedBlockingQueue:无界队列,默认 Integer.MAX_VALUE。我一般不直接用,怕内存溢出。
- ArrayBlockingQueue:有界队列,最推荐。容量根据业务峰值估算。
- DelayQueue:延时执行的任务队列。
我个人最喜欢 ArrayBlockingQueue,搭配合理的容量,比如 1000~5000,既能缓冲流量,又能防止队列无限膨胀。
5. threadFactory(线程工厂)
用来创建新线程,默认的工厂只会生成 "pool-1-thread-1" 这种名字。排查问题时根本分不清是哪个池子。所以我一定会自定义 ThreadFactory,给线程加前缀。比如:
ThreadFactory factory = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "order-pool-" + counter.getAndIncrement());
t.setDaemon(false);
return t;
}
};
这样线上出问题,一看线程名就知道是哪个业务池子,定位速度快很多。
6. RejectedExecutionHandler(拒绝策略)
当线程池和队列都满了,新任务怎么处理?JDK 提供了四种策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛 RejectedExecutionException | 非核心链路,可接受失败重试 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 核心业务,降低吞吐但不能丢任务 |
| DiscardPolicy | 直接丢弃,无提示 | 日志记录等可丢失任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,再提交 | 实时性要求高的场景,保新弃旧 |
我曾经在支付回调场景里用了 AbortPolicy,结果队列满了直接抛异常,回调失败后一直重试,最终导致雪崩。后来改成 CallerRunsPolicy,让主线程帮忙处理,虽然吞吐降了,但保证了不丢任务。嗯,这是血的教训。
注意:CallerRunsPolicy 会让提交任务的线程阻塞,如果主线程是 Tomcat 的请求处理线程,可能导致前端超时。建议配合监控使用。
参数调优的总体思路
调优不等于调大参数。我记得有次帮同事看一个线上故障,他把 corePoolSize 设为 50,maximumPoolSize 设为 200,队列用无界的 LinkedBlockingQueue,结果 core 线程一直在跑,队列却越积越多,最终 OOM。为什么?因为核心线程数已经够处理日常流量,队列又不会触发创建新线程,任务一直在队列里堆积,直到撑爆内存。
所以我的调优原则是:
- 先根据业务类型(CPU 密集型 / IO 密集型)估算 corePoolSize。
- 设置一个有界队列,容量根据峰值 1.5~2 倍估算。
- maximumPoolSize 控制在线程数不会压垮 CPU,一般不超过 (corePoolSize * 3) 或 200。
- keepAliveTime 根据业务峰谷调节,配合动态配置中心使用最佳。
- ThreadFactory 必须自定义,方便排查。
- 拒绝策略选择要谨慎,生产环境推荐 CallerRunsPolicy 或自定义策略(比如降级写 MQ)。
一句话总结:线程池参数不是一次性设死的,需要根据流量、响应时间、CPU 使用率来持续调整。我习惯先压测,再观察监控,最后固化参数。
好了,这一章咱们把线程池的六个核心参数都捋了一遍。从工作流程到每个参数的含义,再到我踩过的坑,应该能帮你建立一个清晰的认知。接下来,你可以在代码里动手试试,用不同的参数组合跑一跑,感受一下它们之间的联动关系。