一、从一个面试题说起:你真的会用线程池吗?
在很多 Java 后端面试里,面试官经常会在聊完 JUC 并发包之后突然抛出这个问题:“你对 Java 线程池了解多少?”如果你只回答“可以复用线程、减少创建销毁开销”,通常只能得到及格分;如果你接着讲核心参数、队列、拒绝策略、执行流程和源码细节,才算真正展示出系统化掌握。
这个问题之所以高频出现,是因为线程池几乎贯穿每一类服务端系统:定时任务、异步下单、消息推送、批量导入、网关聚合、RPC 线程模型、数据库连接池背后的线程调度,都会用到线程池。它既是并发编程的高频考点,也是线上故障的高发地点。
本文会用尽可能完整的视角讲解 Java 线程池,从为什么需要线程池、核心参数、运行机制、源码流程,到队列选择、拒绝策略、动态调参、线上监控和常见踩坑,并给出可以落地使用的示例代码。建议结合 JDK 8 的ThreadPoolExecutor源码一起阅读,效果最好。
二、为什么需要线程池:从线程的昂贵说起
2.1 线程本身的成本
在 JVM 中,每个Thread对象最终都对应操作系统层面的一个内核线程,也就是我们常说的 1:1 线程模型。创建一个线程并不是 new 一个对象那么简单,它会涉及操作系统内核资源的分配、线程栈内存的申请、线程控制块的维护,以及线程调度器的参与。
如果每次执行一个小任务都创建线程,任务执行完又销毁线程,系统会产生两类明显成本:
- 创建和销毁成本高:线程创建时需要申请栈空间、注册到调度器,销毁时又要释放这些资源。
- 上下文切换开销大:线程数量超过 CPU 核心数太多之后,线程之间频繁争抢 CPU,操作系统不得不经常做上下文切换,保存和恢复寄存器、程序计数器、栈指针等现场信息。
更危险的是,如果任务到达速度很快,系统又采用“来一个任务创建一个线程”的方式,线程数量可能在短时间内暴涨。每个线程默认栈大小约 1MB,几千个线程就可能吃掉几个 GB 内存,进而触发OutOfMemoryError。即便没有 OOM,大量线程同时竞争资源也会拖垮 CPU,最终导致系统不可用。
2.2 池化思想:线程池解决什么问题
线程池本质上是“池化技术”的典型应用。它和数据库连接池、对象池、内存池思路一致:预先创建或按需创建一批可复用的资源,任务使用完资源后不销毁,而是归还到池中等待下一个任务继续使用。
线程池主要解决四个问题:
- 降低资源消耗:通过复用已创建的线程,减少线程创建和销毁带来的开销。
- 提高响应速度:任务到达时,如果池中已有空闲线程,可以立即执行,而不需要等待线程创建。
- 提高线程可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统稳定性。线程池可以对线程数量、队列长度、拒绝行为进行统一管理和调优。
- 提供更强大的任务调度能力:例如延迟执行、定时执行、批量执行和返回结果等能力。
但线程池并不是银弹。配置不合理时,线程池可能让问题更隐蔽:任务堆积、线程饥饿、OOM、接口超时等故障,往往都源于对线程池理解不够深入。
三、Java 线程池体系概览
Java 中与线程池相关的核心类型主要在java.util.concurrent包中,最常用的体系如下:
Executor:顶层接口,只有execute(Runnable command)一个方法,表示执行一个任务。ExecutorService:扩展了Executor,增加了任务提交、生命周期管理、批量执行等方法,例如submit、invokeAll、shutdown。AbstractExecutorService:抽象实现类,提供了submit、invokeAll等方法的默认实现,底层委托给execute。ThreadPoolExecutor:核心实现类,也是实际生产中最常使用的线程池实现。ScheduledThreadPoolExecutor:支持定时和周期任务调度的线程池,继承自ThreadPoolExecutor。Executors:工厂工具类,提供若干快捷创建线程池的静态方法。
日常开发中,绝大多数业务代码使用的是ThreadPoolExecutor或者由它衍生出的框架线程池。理解它的内部结构,是回答“你对线程池了解多少”的关键。
四、ThreadPoolExecutor 的七个核心参数
ThreadPoolExecutor最完整的构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 省略初始化细节 }这七个参数决定了线程池的几乎所有行为,必须逐个理解清楚。
4.1 corePoolSize:核心线程数
核心线程数是线程池中会长期保留的线程数量。线程池在没有明确设置allowCoreThreadTimeOut(true)时,核心线程默认不会被回收,即使它们长时间没有任务执行。
核心线程并不是在创建线程池时一次性创建好的。默认情况下,线程池采用的是“懒加载”策略,只有在任务到来时才会创建线程,直到达到corePoolSize。如果希望线程池预热,可以调用prestartAllCoreThreads()或prestartCoreThread()。
4.2 maximumPoolSize:最大线程数
最大线程数是线程池允许创建的最大线程数量。只有当核心线程都处于忙碌状态,并且任务队列也满了的时候,线程池才会创建非核心线程来处理任务。
这里有一个非常常见的误区:线程池不是任务一多就立即创建到最大线程数,而是先填满核心线程,再填满队列,最后才创建非核心线程。如果队列是无界队列,那么maximumPoolSize通常不会生效,线程数最多只能达到corePoolSize。
4.3 keepAliveTime:线程空闲存活时间
该参数表示非核心线程在空闲一段时间后会被回收的时间长度。只有当线程池中的线程数大于corePoolSize时,超过keepAliveTime仍然没有任务可执行的线程才会被销毁。
如果调用allowCoreThreadTimeOut(true),核心线程在一定空闲时间后也可以被回收,这适用于任务有明显波峰波谷的场景。
4.4 unit:时间单位
keepAliveTime的时间单位,常见取值有TimeUnit.SECONDS、TimeUnit.MILLISECONDS、TimeUnit.MINUTES等。
4.5 workQueue:工作队列
工作队列用于保存等待执行的任务。任务提交后,如果核心线程都在忙碌,线程池会先把任务放入工作队列。队列类型直接决定了线程池的任务积压策略和内存风险。
4.6 threadFactory:线程工厂
线程工厂负责创建新线程。通过自定义ThreadFactory,我们可以设置线程名称、是否为守护线程、优先级和异常处理器等。规范的线程名称对线上排查问题非常重要。
4.7 handler:拒绝策略
当线程池已经关闭,或者任务无法被接收时,比如工作线程已达到最大数量并且队列也满了,线程池会通过拒绝策略处理新提交的任务。拒绝策略可以自定义,JDK 也提供了四种内置实现。
下面用一个表格对这七个参数进行总结:
| 参数 | 类型 | 核心作用 | 常见问题 |
|---|---|---|---|
| corePoolSize | int | 核心线程数,长期保留 | 过大会浪费资源,过小可能处理能力不足 |
| maximumPoolSize | int | 最大线程数,包含核心线程 | 队列无界时不会生效 |
| keepAliveTime | long | 非核心线程空闲存活时间 | 需配合 unit 使用 |
| unit | TimeUnit | 空闲时间单位 | 注意单位混淆 |
| workQueue | BlockingQueue | 暂存等待执行的任务 | 无界队列可能导致 OOM |
| threadFactory | ThreadFactory | 创建线程并命名 | 默认命名不友好,难以排查 |
| handler | RejectedExecutionHandler | 任务无法接收时的处理方式 | 默认 AbortPolicy 会抛异常 |
五、线程池的五种运行状态
ThreadPoolExecutor使用一个AtomicInteger类型的ctl字段来同时保存线程池的运行状态和有效线程数。它的高 3 位表示运行状态,低 29 位表示线程数量。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // 29 private static final int CAPACITY = (1 << COUNT_BITS) - 1; private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS;五种状态的含义和转换关系如下:
- RUNNING:接受新任务,并处理队列中的任务。
- SHUTDOWN:不再接受新任务,但会继续处理队列中已经提交的任务。调用
shutdown()后进入该状态。 - STOP:不再接受新任务,也不再处理队列中的任务,同时尝试中断正在执行的任务。调用
shutdownNow()后进入该状态。 - TIDYING:所有任务都已终止,工作线程数为 0,线程池即将进入 TERMINATED 状态。
- TERMINATED:线程池彻底终止,
terminated()钩子方法执行完毕。
状态转换可以概括为:RUNNING 可以通过shutdown()进入 SHUTDOWN,或通过shutdownNow()进入 STOP;SHUTDOWN 和 STOP 都可以在任务清理完成后进入 TIDYING;TIDYING 完成后进入 TERMINATED。RUNNING 不能直接回到其他状态,线程池一旦关闭就不会重新启动。
六、任务提交与执行流程:一条任务的完整旅程
理解execute(Runnable command)的源码流程,是理解线程池运行机制最直接的方式。JDK 8 中的核心实现如下:
public void execute(Runnable command) { if (command == null) { throw new NullPointerException(); } int c = ctl.get(); // 第一步:当前线程数小于核心线程数 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) { return; } c = ctl.get(); } // 第二步:尝试把任务放入工作队列 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (!isRunning(recheck) && remove(command)) { reject(command); } else if (workerCountOf(recheck) == 0) { addWorker(null, false); } } // 第三步:队列放不进去,尝试创建非核心线程 else if (!addWorker(command, false)) { reject(command); } }这段代码可以拆成三步理解:
- 优先使用核心线程:如果当前工作线程数小于
corePoolSize,直接尝试创建新的核心线程执行任务。 - 其次放入工作队列:如果核心线程已满,并且线程池处于 RUNNING 状态,就把任务放入队列。放入后还需要二次检查线程池状态,防止线程池在放入期间被关闭。
- 最后创建非核心线程:如果队列已经满了,才会尝试创建非核心线程。如果线程数已经达到
maximumPoolSize,则执行拒绝策略。
这就是常说的线程池任务处理优先级:核心线程优先,队列其次,最大线程兜底,拒绝策略最后。这个顺序非常重要,因为它解释了为什么很多线程池配置里即使设置了很大的maximumPoolSize,在队列未满时也不会创建非核心线程。
大多数业务场景下,真正能扛住高并发的通常是工作队列,而不是无限扩大线程数。线程创建过多会导致上下文切换频繁,反而降低吞吐量。
七、addWorker:线程是如何被创建出来的
addWorker(Runnable firstTask, boolean core)负责真正创建一个工作线程,并把它加入工作线程集合。它主要做四件事:
- 通过 CAS 增加线程池中的线程数量。
- 根据线程池状态和边界条件判断是否能继续创建线程。
- 创建一个
Worker对象,内部封装一个Thread。 - 启动该线程,让它开始执行任务。
Worker是ThreadPoolExecutor的内部类,它同时实现了Runnable,也继承了AbstractQueuedSynchronizer,既负责执行任务,也用于表示线程是否被任务独占。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; Runnable firstTask; volatile long completedTasks; Worker(Runnable firstTask) { setState(-1); this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } public void run() { runWorker(this); } }可以看到,Worker中创建的线程执行的是Worker自身,而Worker.run()又调用runWorker(this),从而进入任务循环。
定义core参数决定创建的是核心线程还是非核心线程。如果core=true,那么当前线程数不能大于等于corePoolSize;如果core=false,则当前线程数不能大于等于maximumPoolSize。这里使用的是严格小于判断。
八、runWorker 与 getTask:线程如何复用
工作线程之所以能够复用,核心在于runWorker中的循环。它不断从getTask()获取任务并执行,直到获取不到任务才退出。
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // 如果线程池已经 STOP,需要中断线程 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) { wt.interrupt(); } try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }runWorker的核心逻辑可以概括为:
- while 循环是复用的关键:第一次执行
firstTask,之后不断从getTask()拉取新任务。 - 中断处理:在线程池进入 STOP 状态时,即使任务还没有执行完,也会尝试中断当前工作线程。
- 钩子方法:
beforeExecute和afterExecute提供了任务执行前后的扩展点,可用于埋点、日志和监控。 - 异常兜底:任务抛出的异常会被捕获并重新抛出,保证工作线程不会因普通业务异常而直接终止。
- 退出清理:无论正常结束还是异常退出,最终都会进入
processWorkerExit,完成线程数量扣减和补充线程等收尾工作。
真正决定线程何时退出、空闲线程如何被回收的,是getTask()。
private Runnable getTask() { boolean timedOut = false; for (;;) { int c = ctl.get(); int rs = runStateOf(c); if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) { return null; } continue; } try { Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) { return r; } timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }getTask的退出逻辑体现了线程回收的设计:核心线程在没有空闲超时设置时通过阻塞take()一直等待任务;非核心线程使用带超时的poll(),超过keepAliveTime没有取到任务就返回null,线程随之退出循环并被回收。
九、四种拒绝策略:线程池满了之后怎么办
当工作队列已满且线程数达到maximumPoolSize,或者线程池已经关闭时,新提交的任务会被拒绝。JDK 提供了四种内置拒绝策略:
- AbortPolicy:默认策略,直接抛出
RejectedExecutionException,适合要求快速失败、希望调用方立刻感知的场景。 - CallerRunsPolicy:由提交任务的调用线程直接执行该任务,既能降低新任务涌入速度,又能避免任务被无声丢弃。
- DiscardPolicy:直接丢弃被拒绝的任务,不抛异常,也不做任何处理,风险较高。
- DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新提交当前任务。
实际开发中,很多人会自定义RejectedExecutionHandler,例如把被拒绝的任务写入消息队列或数据库,以便后续补偿处理。理解这四种策略的语义,是避免线上任务被“悄悄吞掉”的基础。
十、工作队列选择策略
workQueue的选择会直接影响线程池的扩容行为和内存风险。常见队列主要有以下三类:
- SynchronousQueue:不存储任务,每个插入操作必须等待另一个线程进行移除操作。它能让“核心线程忙不过来的任务”直接触发非核心线程创建,适合对响应速度要求很高、任务量短时突增的场景。
- LinkedBlockingQueue:默认无界队列,任务是先进先出、无限堆积。它会导致
maximumPoolSize不生效,任务过多时存在 OOM 风险。 - ArrayBlockingQueue:有界队列,可以明确限制任务积压数量。它能让线程池在队列满后创建非核心线程,并在仍无法处理时触发拒绝策略,是最推荐用于生产环境的队列类型之一。
一个比较稳妥的生产实践是:使用有界队列,把maximumPoolSize的扩容能力和队列容量配合起来,让线程池在“快速处理”和“防止内存打爆”之间取得平衡,同时配合合适的拒绝策略兜底。
十一、动态调参与线上监控
ThreadPoolExecutor提供了多个可以在运行时调整参数的方法:
// 调整核心线程数 executor.setCorePoolSize(8); // 调整最大线程数 executor.setMaximumPoolSize(16); // 是否允许核心线程超时回收 executor.allowCoreThreadTimeOut(true); // 调整队列容量需要创建新的有界队列,并替换原有队列 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());线上监控主要关注以下指标:
- 任务总数与已完成数:通过
getTaskCount()与getCompletedTaskCount()观察任务吞吐。 - 当前线程数与池大小:通过
getPoolSize()、getCorePoolSize()判断线程数是否经常接近上限。 - 队列积压:通过
getQueue().size()观察任务堆积情况,避免任务长时间等待。 - 被拒绝的任务数:在自定义拒绝策略中记录计数,及时发现配置不合理。
动态调参尤其适合任务量有波峰波谷的场景:高峰期前调大核心线程和队列容量,低谷期后再缩小资源,避免线程长期空闲占用。
十二、常见踩坑与最佳实践
线程池用得好是利器,用不好就是故障源头。以下是最常见的几个坑:
- 用 Executors 的快捷方法创建线程池:
Executors.newFixedThreadPool和Executors.newCachedThreadPool默认使用无界队列或允许无限创建线程,规范上更推荐显式使用ThreadPoolExecutor构造方法。 - 无界队列导致线程数上不去:以为设置了大
maximumPoolSize就能扩容,结果队列一直不满,线程数始终卡在核心线程数。 - 拒绝策略选择不当:使用
DiscardPolicy或DiscardOldestPolicy时,任务丢失往往没有日志,故障很难排查。 - 线上线程没有命名:线程名全是
pool-1-thread-1这类默认名称,排查堆栈和日志时很难定位来源。 - 忘记关闭线程池:应用下线时线程池仍然存活,可能导致进程无法正常退出。建议结合
shutdown()或awaitTermination()做优雅停机。
综合来看,生产环境的推荐实践是:显式使用ThreadPoolExecutor构造方法,设置友好的线程名前缀,使用有界队列,结合业务选择拒绝策略,并通过监控指标持续观察线程数、队列长度和被拒绝任务数。
十三、总结
Java 线程池的本质,是用一组可复用的工作线程和一整套任务管理机制,把“线程”这一昂贵资源的使用权收归统一调度。它的核心逻辑可以浓缩为三点:线程如何被创建、任务如何被排队处理、线程如何被复用和回收。
面试和实际开发中,如果能沿着execute的提交流程往下讲,再到addWorker、runWorker和getTask,同时把七参数、队列类型、拒绝策略和监控指标串起来,就基本覆盖了线程池的主要知识点。更重要的是,这些知识能帮助我们在线上定位任务堆积、线程饥饿、OOM 和接口超时等真实问题。