线程池原理:ThreadPoolExecutor的工作机制、任务队列与工作线程、线程池的生命周期
说实话,线程池这东西,很多同学一开始觉得不就是个「池子」嘛,往里扔任务就行了。但真到线上出问题的时候,才发现自己根本不了解它内部是怎么转的。我当年刚带团队时,就吃过一次亏——线程池参数拍脑袋配的,结果高峰期直接打满,任务全堵在队列里,最后OOM了。嗯,从那以后,我老老实实把源码翻了一遍。
今天咱们就把 ThreadPoolExecutor 的底裤扒干净。你想想看,它到底是怎么接任务、怎么派活、怎么决定要不要新招人的?
一、ThreadPoolExecutor 的核心工作机制
说白了,ThreadPoolExecutor 就是一个「任务调度中心」。它内部维护了一组工作线程,外加一个任务队列。外部提交的任务,不会直接创建线程去跑,而是先走一套决策流程。
我习惯把这个流程总结成三步:
- 判断核心线程:当前工作线程数 < corePoolSize?是的话,直接新建线程跑任务。
- 丢进队列:核心线程满了?那就把任务塞进阻塞队列,等着空闲线程来取。
- 判断最大线程:队列也满了?那就看看能不能再创建线程,直到达到 maximumPoolSize。
如果队列满了,线程数也到上限了,那就触发拒绝策略。这个顺序,我建议你刻在脑子里。
为什么会这样设计?其实就是为了「削峰填谷」。核心线程处理日常流量,队列缓冲突发流量,最大线程应对极限压力。我在项目中遇到过,有些同学把 corePoolSize 设得很大,觉得这样响应快。结果呢?线程上下文切换把CPU干到100%,反而更慢了。
二、任务队列:阻塞队列的选择
任务队列是线程池的「蓄水池」。不同的队列类型,直接决定了线程池的行为。我个人最常用的就三种:
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
SynchronousQueue |
不存储任务,直接转交 | 高并发、低延迟,希望任务立刻被处理 |
LinkedBlockingQueue |
无界队列(默认Integer.MAX_VALUE) | 任务量可控,不希望拒绝任务 |
ArrayBlockingQueue |
有界队列,容量固定 | 需要控制内存占用,防止OOM |
这里有个坑,我必须要说。用 LinkedBlockingQueue 如果不设容量,那就是无界队列。任务积压起来,内存会一直涨。我曾经接手过一个遗留系统,线程池用的无界队列,结果一次促销活动,队列里堆了上千万个任务,直接内存溢出。嗯,从那以后,我只要用线程池,队列必设上限。
三、工作线程:Worker 的诞生与消亡
线程池里的工作线程,其实被包装成了一个内部类——Worker。它本身实现了 Runnable 接口,内部持有一个真正的线程对象。
Worker 的生命周期很简单:
- 创建:当需要新线程时,通过线程工厂创建。
- 运行:不断从任务队列里取任务,执行。
- 回收:如果线程空闲时间超过
keepAliveTime,并且当前线程数 > corePoolSize,就会被销毁。
你想想看,Worker 是怎么做到「一直活着等任务」的?其实就是个 while(true) 循环,里面调用 workQueue.take() 或 workQueue.poll(keepAliveTime, TimeUnit)。take 会阻塞直到有任务,poll 会超时返回 null,然后线程就退出了。
// 简化后的 Worker 运行逻辑
while (task != null || (task = getTask()) != null) {
// 执行任务
task.run();
task = null;
}
// 线程退出,Worker 被回收
这里有个细节:核心线程默认是不会被回收的,除非你设置了 allowCoreThreadTimeOut(true)。我建议,如果你的业务有明显的闲时,可以开启这个选项,节省资源。
四、线程池的生命周期
线程池不是创建了就能一直用的。它有自己的状态流转。源码里定义了5种状态:
- RUNNING:正常运行,接受新任务,处理队列任务。
- SHUTDOWN:不接受新任务,但会处理完队列里的任务。
- STOP:不接受新任务,也不处理队列任务,还会中断正在执行的任务。
- TIDYING:所有任务都终止了,工作线程数为0,即将执行 terminated() 钩子方法。
- TERMINATED:terminated() 执行完毕,线程池彻底结束。
状态流转是单向的,只能从 RUNNING 往下走,不能回退。我习惯用一张图来理解:
这张图我建议你保存下来。每次调优线程池参数前,先想想当前处于什么状态。我记得有一次排查线上问题,发现线程池一直卡在 SHUTDOWN 状态,原因是队列里有个任务死循环了,永远消费不完。最后只能强制重启。
shutdown() 后,可以用 awaitTermination(long timeout, TimeUnit unit) 等待线程池真正结束。超时后如果还没结束,再考虑 shutdownNow() 强制关闭。
五、总结一下
ThreadPoolExecutor 的工作机制,说白了就是一套「三级缓冲」:核心线程是第一级,任务队列是第二级,最大线程是第三级。每一级都有明确的触发条件。
我个人觉得,理解线程池的关键不在于背参数,而在于理解它的「拒绝优先级」和「状态流转」。你想想看,如果队列满了,线程数也到上限了,这时候新任务来了怎么办?是抛异常?还是让调用方自己跑?这就是拒绝策略的事了,咱们下一节再细聊。
嗯,最后送大家一句话:线程池参数不是配一次就完事的,它需要根据业务流量持续调优。 我每次上线新系统,都会先压测一轮,看看线程池的实际表现,再微调参数。