Java 线程池详解
线程池把「创建线程」这件昂贵的事变成一次性投入:线程用后归还、反复执行新任务,同时给并发度装上明确的上限。本篇把 ThreadPoolExecutor 的七大参数、任务从提交到被拒绝的完整链路、四种内置拒绝策略,以及 shutdown/shutdownNow/awaitTermination 的关闭语义一次讲透。
它解决什么问题
线程不是免费资源:每创建一个线程,JVM 都要为它分配约 1MB 的栈空间并发起系统调用,销毁同样有成本。如果每来一个任务就 new 一个 Thread,高并发下光是创建销毁的开销就足以拖垮服务,更别提线程数量失控带来的内存耗尽。线程池用「池化」思路回应这个矛盾,带来三个直接收益:
- 降低开销:线程执行完任务后不销毁,回到池中等待下一个任务,省去反复创建销毁的成本。
- 加快响应:池中常驻线程随取随用,任务到达即可执行,不必先等线程创建。
- 便于管理:线程数量、排队容量、超载行为都有边界,池还自带运行统计(任务总数、活跃线程数等),方便监控与调优。
Executor 框架把「任务」(Runnable/Callable)、「执行器」(ThreadPoolExecutor 等)和「异步结果」(Future)三件事解耦,其中 ThreadPoolExecutor 是整个框架的核心实现,也是本篇的主角。
核心原理
七大参数逐一拆解
ThreadPoolExecutor 最完整的构造方法接收七个参数,它们共同决定一个线程池的全部行为:
public ThreadPoolExecutor(int corePoolSize, |
- corePoolSize(核心线程数):线程池长期维持的线程规模。默认按需创建,即使空闲也不回收(除非调用
allowCoreThreadTimeOut(true))。 - maximumPoolSize(最大线程数):线程数的上限。只有当队列满了,线程池才会在核心数之上继续扩招,最多到这个值。
- keepAliveTime(空闲存活时间):超出核心数的线程空闲多久后被回收。
- unit:keepAliveTime 的时间单位。
- workQueue(任务队列):核心线程忙不过来时新任务在此排队。常用实现有
ArrayBlockingQueue(有界)、LinkedBlockingQueue(可选容量,默认近无界)、SynchronousQueue(零容量,直接交接)。 - threadFactory(线程工厂):定制线程的创建过程,最常见的用途是给线程起有意义的名字。
- handler(拒绝策略):线程数到顶且队列已满(或池已关闭)时,决定如何处置新任务。
任务调度流程
调用 execute() 提交任务后,线程池按固定顺序决策:
- 工作线程数 < corePoolSize:新建核心线程,立即执行任务。
- 达到 corePoolSize:尝试把任务放入队列等待。
- 队列已满且线程数 < maximumPoolSize:新建非核心线程执行任务。
- 线程数已达 maximumPoolSize 且队列也满:触发拒绝策略。
这里有个反直觉的关键点:非核心线程的扩招排在入队之后。队列只要没满,线程数就永远停在核心数,maximumPoolSize 形同虚设——这正是无界队列的陷阱所在,后文还会展开。另外,非核心线程执行完手头任务后并不会立刻消失,而是和核心线程一样继续从队列抢任务,只是它用带超时的 poll(keepAliveTime) 取任务,超时取不到才退出销毁。
深入机制
execute() 的三段判断
public void execute(Runnable command) { |
这段代码与上面的流程一一对应。值得留意的是入队后的 recheck:入队的瞬间线程池可能恰好被关闭,或者 worker 恰好全部退出,所以要二次检查状态,必要时把任务移出队列走拒绝,或补一个空任务线程兜底。线程池用单个 AtomicInteger ctl 的高 3 位存运行状态、低 29 位存线程数,一次 CAS 同时读写两者,这是 execute 这条高频路径仍能保持低开销的关键设计。
四种拒绝策略
线程池已关闭,或者线程数到顶且队列也塞满时,RejectedExecutionHandler 登场。JDK 内置四种:
- AbortPolicy(默认):抛出
RejectedExecutionException。失败被暴露给调用方,适合不允许丢任务的核心链路,但业务必须捕获异常并做补偿(重试、落库待补偿等)。 - CallerRunsPolicy:由提交任务的线程自己执行该任务。提交速度因此被拖慢,形成天然的背压;代价是提交线程被占用——若它是 Tomcat 请求线程,会直接拉高接口延迟。池已关闭时该策略会丢任务。
- DiscardPolicy:静默丢弃,不留任何痕迹。适用于日志上报、指标采集这类可丢失的任务,排查问题时要有「任务可能悄悄没了」的意识。
- DiscardOldestPolicy:丢弃队列头部最老的任务,再重试入队新任务。适合只关心最新数据的场景;但搭配
PriorityBlockingQueue时弹出的可能是优先级最高的任务,慎用。
内置策略不满足需求时可以自定义,常见思路是记录拒绝计数、把任务写入可靠存储等待补偿。
关闭语义:shutdown / shutdownNow / awaitTermination
三个方法常被混用,语义其实分工明确:
- shutdown():温和关闭。不再接收新任务,队列中已提交的任务继续执行完,正在执行的任务不受打扰。方法立即返回,不等待终止。
- shutdownNow():强制关闭。尝试中断所有正在执行的任务(发 interrupt 信号),并把队列中未执行的任务以
List<Runnable>返回。注意它只是发信号,任务不响应中断就无法保证停止。 - awaitTermination(timeout, unit):阻塞等待线程池进入 TERMINATED 状态,返回值表示是否在超时前等到。
状态流转是单向的:RUNNING → SHUTDOWN → TIDYING → TERMINATED,或 RUNNING → STOP → TIDYING → TERMINATED。标准的优雅关闭模板是三者组合:
executor.shutdown(); |
工程实践
为什么不能直接用 Executors
Executors 的工厂方法看似省事,隐患全藏在默认值里:
- newFixedThreadPool / newSingleThreadExecutor:队列是容量为
Integer.MAX_VALUE的LinkedBlockingQueue。任务提交快于消费时无限堆积,每个任务对象都占堆内存,最终 OOM。且队列永远满不了,maximumPoolSize 和 keepAliveTime 全部失效。 - newCachedThreadPool:队列是零容量的
SynchronousQueue,最大线程数却是Integer.MAX_VALUE。任务执行慢、提交快时线程无限膨胀,CPU 和内存双双被打爆,同样是 OOM。 - newScheduledThreadPool:内部的
DelayedWorkQueue自动扩容、近乎无界,延迟任务同样有堆积风险。
问题不在于这些方法本身,而在于参数不透明——使用者往往意识不到自己接过的是一个没有刹车的池子。生产环境应直接 new ThreadPoolExecutor,把上限写明白:
ThreadPoolExecutor pool = new ThreadPoolExecutor( |
有界队列、命名线程工厂、显式拒绝策略,这三件事是底线。
队列选择的权衡
| 队列 | 容量特性 | 对线程数的影响 | 适用场景 |
|---|---|---|---|
| ArrayBlockingQueue | 固定有界 | 队列满才扩到 max | 需要明确容量上限 |
| LinkedBlockingQueue | 默认近无界,可指定容量 | 无界时 max 失效,常驻 core | 任务量平稳且设了容量 |
| SynchronousQueue | 零容量 | 任务直接交接,容易触达 max | 追求低排队延迟 |
| PriorityBlockingQueue | 无界,按优先级出队 | max 失效 | 任务有优先级差异 |
大队列能平滑流量,但会隐藏过载、拉长任务等待时间;小队列加更多线程响应更快,上下文切换成本也更高。通常宁可让拒绝策略尽早暴露问题,也不要用巨型队列掩盖容量不足。
面试视角
execute() 提交任务后的完整流程是什么? 线程数不足 core 先建核心线程;达到 core 后任务入队;队满且未到 max 建非核心线程;全满走拒绝策略。务必记住非核心线程的创建排在入队之后,所以无界队列下 max 永远不生效。
为什么不推荐 Executors 的工厂方法? Fixed 和 Single 用近无界队列,任务持续堆积导致 OOM;Cached 线程数无上限,线程爆炸耗尽 CPU 和内存;根本问题是默认值把资源风险藏了起来。
shutdown() 和 shutdownNow() 有什么区别? 前者不再接收新任务,但会把队列中已提交的任务执行完,且不中断执行中的任务;后者尝试中断所有任务,并返回未执行的任务列表。两者都立即返回,需配合 awaitTermination 等待真正终止。
核心线程会被回收吗? 默认不会,空闲也常驻;调用 allowCoreThreadTimeOut(true) 后,核心线程空闲超过 keepAliveTime 同样会被回收,这在负载波动大的场景下能省资源。
CallerRunsPolicy 有什么副作用? 任务在提交线程中执行,提交被拖慢形成背压;但提交线程若是 Web 请求线程,会占用它并拉高接口耗时。线程池已关闭时,该策略直接丢弃任务。
小结
- 七大参数里 core、max、workQueue 决定容量结构,keepAliveTime 管回收,threadFactory 和 handler 管创建与兜底。
- 「核心线程 → 队列 → 非核心线程 → 拒绝」是理解一切线程池现象的总纲,无界队列会让 maximumPoolSize 失效。
- 四种拒绝策略没有银弹:不能丢用 Abort 加补偿,要背压用 CallerRuns,可丢弃用 Discard,只认最新用 DiscardOldest。
- shutdown 温和、shutdownNow 强制、awaitTermination 等待,三者组合才是完整的优雅关闭。
- 生产环境放弃 Executors 工厂方法,用有界队列和显式参数把风险控制在配置里。

