Java 线程池原理详解:从核心参数到线上调优
线程池大概是 Java 里「用起来最简单、却最容易出事故」的组件。创建只要一行代码,七个参数看着也都懂,可真到了线上,问题往往长这样:接口 P99 从 80ms 涨到 3s,日志里一条 RejectedExecutionException 都没有,线程数却一直是 8,队列已经排了三万个任务;或者服务突然 unable to create new native thread,重启后半小时再次复现。
这些现象的背后,其实都指向同一件事:很多人记住了七个参数的名字,但没有记住参数之间真实的生效顺序。所以这篇文章不按「参数说明书」来讲,而是按一次任务提交的真实路径展开:先看线程池解决了什么问题,再拆开七大参数与 ctl 的位运算,然后用 execute() 源码走一遍「核心线程 → 队列 → 最大线程 → 拒绝」这条决策链,接着讲线程是怎么被复用的、池是怎么关闭的、队列怎么选、线程数怎么算,最后落到线上监控、动态调参和故障排查。
全文配 11 张原创 SVG 图解和 12 段代码,读完你应该能同时拿下两类问题:面试里从「线程池有哪几个参数」到「为什么阿里不让用 Executors」的整条追问链,以及线上真出故障时该看哪个指标、敲哪条命令。
1. 为什么需要线程池:先把成本算清楚
Java 线程是操作系统线程的 1:1 映射(HotSpot 下 Thread.start0() 最终调用 pthread_create)。这意味着每创建一个线程,JVM 都要向操作系统申请资源:
- 线程栈内存:默认 1MB(
-Xss),1000 个线程仅栈就吃掉约 1GB 虚拟内存; - 内核态开销:创建/销毁线程涉及用户态与内核态切换,CPU 时间被消耗在调度器上,而不是业务逻辑上;
- 上下文切换:线程数超过 CPU 核数后,切换成本开始抵消并行收益,常见量级是每次切换几微秒;
- 线程上限:受
ulimit -u、kernel.threads-max、内存等限制,一旦触顶就是OutOfMemoryError: unable to create new native thread,这类错误通常无法靠加大堆内存解决。
线程池的价值就是把上面这些成本摊薄,并额外拿到两样东西:可控的并发上限(这本身就是一种限流)和统一的管理入口(命名、监控、拒绝策略、优雅关闭)。

2. 七大参数与 ctl:先看清内部结构
线程池的完整行为由七个参数决定。先把构造函数摆在明面上:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // keepAliveTime 的时间单位
BlockingQueue<Runnable> workQueue, // 任务等待队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) { // 拒绝策略
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize
|| keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这七个参数里,有四个直接决定容量行为(core、max、keepAlive、queue),一个决定线程长什么样(factory),一个决定过载时怎么处理(handler),还有一个是时间单位。下面这张图把它们的职责与运行时组件对应起来:

ctl 是理解线程池状态管理的关键。JDK 用了一个很巧的设计:把「运行状态」和「线程数量」压进同一个 AtomicInteger 的 32 位里,高 3 位存状态(5 个状态需要 3 位),低 29 位存线程数,这样一次 CAS 就能同时保证两者的原子性。

状态比较用 < 就能判断「是否已经关闭」,代码里到处都是 isRunning(c)、rs >= SHUTDOWN 这类写法,根源就在状态值被设计成了单调递增的整数。
3. 任务提交:execute() 的完整决策链
所有的行为分歧都发生在 execute() 里。把 JDK 8 的源码逐段拆开看:
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
// ① 线程数 < core:直接新建核心线程执行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) return;
c = ctl.get();
}
// ② 线程数已达 core:先尝试入队
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);
}
}
// ③ 入队失败(队列满):再扩到 max
else if (!addWorker(command, false)) {
// ④ 已达 max 且队列满:拒绝
reject(command);
}
}
这段代码里藏着三个高频误解:
**误解一:线程数到 core 之后继续建线程。**事实相反,先入队。这是为了尽量复用已创建的线程,避免线程频繁创建销毁。只有当队列也满了,才会把线程数扩到 maximumPoolSize。
**误解二:maximumPoolSize 是「线程数上限」,所以设大点没坏处。**在无界队列(如 LinkedBlockingQueue 不传容量)下,队列永远不会满,第 ③ 步根本走不到,maximumPoolSize 形同虚设——这正是 Executors.newFixedThreadPool 的隐患所在。
**误解三:入队成功就一定会被执行。**不一定,入队后还会 recheck 一次:如果此刻线程池已经关停,任务会被 remove() 回滚出队并交给拒绝策略,避免任务滞留在队列里永远不执行。

**一句话记住顺序:**先看核心线程,再看队列,最后才看最大线程。这个顺序决定了线程池的一个反直觉特性——线程池是否「愿意」扩容,取决于你的队列有多小。想让线程数快速涨起来,就要用小容量队列(如 ArrayBlockingQueue)或 SynchronousQueue。
4. 线程复用:Worker 与 runWorker 循环
线程池能复用线程,靠的是 Worker 类。它是一个 AbstractQueuedSynchronizer 子类(因此本身就是一个锁),同时实现了 Runnable:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread; // Worker 持有并启动的线程
Runnable firstTask; // 第一个任务(可为 null)
volatile long completedTasks; // 已完成任务数(监控用)
Worker(Runnable firstTask) {
setState(-1); // 禁止在 runWorker 前被中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // 把自己交给线程
}
public void run() { runWorker(this); } // 线程启动后进入循环
}
关键在于 runWorker(this) 里的这个 while 循环——线程执行完一个任务后不会退出,而是继续去队列里取下一个任务:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 允许中断
boolean completedAbruptly = true;
try {
// 循环:先执行首任务,然后不断 getTask() 从队列取任务
while (task != null || (task = getTask()) != null) {
w.lock(); // 加锁:shutdownNow 时才允许中断
try {
beforeExecute(wt, task); // 扩展点:埋点、MDC 传递
try {
task.run(); // 执行用户任务
} finally {
afterExecute(task, null); // 扩展点:清理 ThreadLocal 等
}
} finally {
task = null; w.completedTasks++; w.unlock();
}
}
completedAbruptly = false;
} finally {
// 循环退出:移除 Worker,必要时补线程
processWorkerExit(w, completedAbruptly);
}
}
而 getTask() 决定了「什么时候这个线程该退出」:
- 线程池已
STOP或正在关闭且队列为空 → 返回null,线程退出; - 当前线程数超过
corePoolSize(或设置了allowCoreThreadTimeOut)→ 用poll(keepAliveTime)超时获取,等不到任务就返回null; - 否则用
take()无限阻塞等任务——这就是核心线程「常驻」的实现方式; - 返回
null前会 CAS 减少workerCount,保证线程数统计准确。

**实操建议:**因为 beforeExecute / afterExecute 是线程池留出的官方扩展点,MDC 透传、任务耗时埋点、ThreadLocal 清理都应该写在这两个方法里(覆写 ThreadPoolExecutor 子类),而不是散落在每个业务任务的 try-finally 中。
5. 五种运行状态与优雅关闭
线程池不是「开着就一直开着」的。它由 ctl 高 3 位维护五种状态,流转方向基本是单向的:
| 状态 | 值 | 含义与行为 |
|---|---|---|
RUNNING | -536870912 | 接受新任务,并处理队列中的任务 |
SHUTDOWN | 0 | 不再接受新任务,但会继续处理完队列里的任务;中断空闲线程 |
STOP | 536870912 | 不再接受新任务,不处理队列任务,并中断正在执行的任务 |
TIDYING | 1073741824 | 所有任务已终止、线程数为 0,即将执行 terminated() 钩子 |
TERMINATED | 1610612736 | terminated() 执行完毕,线程池彻底结束 |

shutdown() 与 shutdownNow() 的差异经常被面试追问,也经常在生产上出错:
shutdown():状态置为SHUTDOWN,中断所有空闲线程(interruptIdleWorkers),队列中剩余任务会继续执行完;shutdownNow():状态置为STOP,中断所有线程(包括正在执行任务的),并返回队列中未执行的任务列表。注意:中断只是「通知」,如果任务不响应中断(没有InterruptedException或isInterrupted()检查),它会继续跑完。
因为 shutdown() 不会等待任务执行完,生产上稳妥的关闭姿势是「先温和、再等待、最后强制」:
public void gracefulShutdown(ThreadPoolExecutor pool, long timeout, TimeUnit unit) {
pool.shutdown(); // ① 停止接收新任务,队列任务继续跑
try {
if (!pool.awaitTermination(timeout, unit)) { // ② 等待一段「合理」时间
List<Runnable> dropped = pool.shutdownNow(); // ③ 超时则强制中断
log.warn("线程池未能优雅关闭,丢弃 {} 个任务", dropped.size());
pool.awaitTermination(5, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt(); // 恢复中断标记,别吞掉
}
log.info("线程池已关闭,活跃线程={}, 队列剩余={}",
pool.getActiveCount(), pool.getQueue().size());
}
**容易漏掉的一点:**Web 应用里线程池如果不随容器关闭而关闭,会表现为「应用明明重启了,日志还在打旧任务的输出」,或者类加载器无法卸载导致 Metaspace 缓慢泄漏。Spring 环境下让线程池实现 DisposableBean / SmartLifecycle,或在 @PreDestroy 里调用上面的关闭方法;Servlet 容器也可以注册 ServletContextListener 兜底。
6. 拒绝策略:过载时唯一能做的「正确的事」
当队列满且线程数达到 maximumPoolSize 时,线程池必须做出选择。JDK 提供了四种策略,它们的差别远不止「抛不抛异常」:

四种策略都不能满足「先记一笔、再短暂重试、实在不行才失败」这类诉求,所以生产上常见做法是自定义一个:
public class MonitoringRejectPolicy implements RejectedExecutionHandler {
private static final Logger log = LoggerFactory.getLogger(MonitoringRejectPolicy.class);
private final Counter rejectedCounter; // 指标:Micrometer / Prometheus Counter
public MonitoringRejectPolicy(Counter rejectedCounter) {
this.rejectedCounter = rejectedCounter;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
rejectedCounter.increment();
log.error("线程池过载拒绝:poolSize={}, activeCount={}, queueSize={}, largestPoolSize={}",
e.getPoolSize(), e.getActiveCount(), e.getQueue().size(), e.getLargestPoolSize());
throw new RejectedExecutionException("task queue is full, rejected by policy");
}
}
一个反直觉的结论:CallerRunsPolicy 常常是「不加机器也不崩」的最优解——它把压力原路退回给调用方,让上游的 QPS 自然下降。但它有两个前提:调用方线程允许被占用较长一段时间(不能是 Netty EventLoop 这类 I/O 线程),并且下游任务一定会结束(如果任务会阻塞很久,等于把 Tomcat 线程池一起拖死)。
7. 队列选择与 Executors 的坑
队列是线程池里最被低估的参数。它的类型和容量,直接决定了线程池过载时会「堆积」还是「扩容」还是「拒绝」:
| 队列类型 | 是否有界 | 特点 | 典型用途 |
|---|---|---|---|
LinkedBlockingQueue | 可无界(默认 Integer.MAX_VALUE) | 链表结构,吞吐高;不传容量即无界,maximumPoolSize 失效 | 容量可控的缓冲队列(务必指定容量) |
ArrayBlockingQueue | 有界 | 数组 + 单锁,容量固定,队列满会触发扩容与拒绝 | 需要强背压、容量明确的场景 |
SynchronousQueue | 无容量 | 不存储元素,必须有线程立刻接手,否则扩容或拒绝 | 任务极短、希望线程数弹性伸缩 |
PriorityBlockingQueue | 无界 | 按优先级出队,无容量限制,存在堆积风险 | 任务有明确优先级(告警优先于报表) |
DelayQueue | 无界 | 延迟到期才能取,常配合定时任务 | 延迟执行、重试退避 |
队列容量不是一个可以随手填的数字,它等于「你允许任务最多等多久」。一个可用的估算方式:
- 先明确可接受的排队时延上限,比如核心接口 P99 排队不超过 200ms;
- 再按稳态处理能力反推:
队列容量 ≈ (峰值 QPS − 最大处理能力) × 允许排队时长; - 例如峰值 2000 QPS、最大处理能力 1200 QPS、允许等 0.2s → 容量约 160;
- 再加上一点余量(如 1.5 倍)与明确的拒绝兜底,就得到具体数值。填完不要当结论,压测校准。

所以更推荐手写创建,把每个参数的选择理由都留在代码里:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // core:按下方公式估算,先给初值
16, // max:队列满时的应急容量
60L, TimeUnit.SECONDS, // 非核心线程空闲 60s 回收
new ArrayBlockingQueue<>(200), // 有界队列:让扩容与拒绝真正生效
new NamedThreadFactory("order-async-"), // 可读线程名 + 异常处理器
new MonitoringRejectPolicy(rejectedCounter));
pool.allowCoreThreadTimeOut(true); // 低峰期连核心线程一起回收
pool.prestartAllCoreThreads(); // 启动即预热,避免首波请求慢
**三个「一上手就该加」的配置:**① 自定义 ThreadFactory 给线程起名(order-async-1 比 pool-3-thread-1 在 jstack 里价值大得多);② 线程工厂里设置 UncaughtExceptionHandler,否则任务抛出的异常会被线程池吞掉,只留下「任务莫名消失」的诡异现象;③ prestartAllCoreThreads() 预热,避免冷启动后的第一批请求等待线程创建。
8. 核心线程数到底怎么定
「线程数设多少」是线程池里最没有标准答案、但最有思考路径的一个问题。可以从任务类型切入:
- CPU 密集型(加解密、序列化、复杂计算、图片处理):线程数约等于
CPU 核数 + 1。多的线程只会增加上下文切换。加 1 是为了在某个线程偶发缺页中断或被短暂阻塞时,CPU 不空转。 - IO 密集型(远程调用、数据库查询、文件读写):线程的大部分时间在等待,可以用更多线程填满 CPU。经典公式是
线程数 = 核数 × (1 + 等待时间 / 计算时间)。纯等待型任务(例如只有一次 RPC)等待/计算比很高,2N~2N+1 是常见经验起点。 - 混合型:优先考虑拆分,把 CPU 密集与 IO 密集任务分到不同线程池做隔离,避免互相拖累;无法拆分时按占比加权估算,或干脆按 IO 型设置后再压测收敛。

把公式落到代码里,最实用的版本不是算出一个精确数字,而是「先算、再观测、再调」:
// 例:单个任务 = 2ms 计算 + 8ms 等 RPC,8 核机器
int cores = Runtime.getRuntime().availableProcessors(); // 8
double waitRatio = 8.0 / (8.0 + 2.0); // 等待占比 0.8
int coreSize = (int) Math.ceil(cores / (1 - waitRatio)); // 40
// 但 40 只是理论值:还要受下游连接池上限、下游承载能力约束
int core = Math.min(coreSize, 32); // 被下游限流能力压回来
log.info("估算 core={}(理论上限 {},受下游容量约束)", core, coreSize);
**Little's Law 视角:**并发数 = 到达率 × 平均处理时间。如果线程池要支撑 500 QPS、单任务平均 100ms,那么稳态需要的并发就是 50 个线程——这与「公式」可以互相校验。用这个视角看问题时,你会发现「线程数」本质上是在为下游的响应时间买单。
9. 线上调优与故障排查
9.1 先建立可观测性
线程池的六个指标几乎覆盖了所有故障场景,值得全部接入监控:
| 指标 | 含义 | 异常时的信号 |
|---|---|---|
getActiveCount() | 正在执行任务的线程数 | 长期等于 max → 容量不足,任务在排队 |
getPoolSize() | 当前线程总数 | 长期等于 core 且队列在涨 → 队列太大 / max 失效 |
getQueue().size() | 队列积压长度 | 持续上升 → 消费能力跟不上,即将雪崩 |
getLargestPoolSize() | 历史峰值线程数 | 远小于 max → max 设大了;等于 max → 曾打到上限 |
getCompletedTaskCount() | 累计完成数 | 增速骤降 → 处理能力劣化 |
| 拒绝次数(自定义计数器) | 过载次数 | 任何非零都值得看一眼,突增意味着容量已到顶 |

9.2 动态调参为什么能生效
很多人以为 setCorePoolSize 只是改个字段,其实它会主动「推」线程一把:
// JDK 8 ThreadPoolExecutor#setCorePoolSize(简化)
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0) throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize) {
interruptIdleWorkers(); // 缩容:中断空闲线程,让它们从 getTask() 退出
} else if (delta > 0) {
// 扩容:有多少新容量就补多少线程(最多补到新 core 值)
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty()) break; // 队列都空了,不用再补
}
}
}
private void interruptIdleWorkers() {
// 关键实现:tryLock() 成功 = 该 Worker 空闲(正在执行任务时锁被持有)
// 因此这一操作只会唤醒空闲线程,不会打断正在执行的任务
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try { t.interrupt(); } finally { w.unlock(); }
}
}
}
这里 Worker 的 AQS 锁设计就体现出价值了:正在执行任务的 Worker 持有锁,所以不会被误中断。同时也解释了一个常被误解的点——interruptIdleWorkers() 的名字确实准确,它只处理空闲线程。
9.3 四类典型线上故障与排查动作

两条常用命令,值得贴进值班手册:
# ① jstack 找线程池里的阻塞点:看线程名(所以一定要自定义 ThreadFactory)
jstack <pid> | grep -A 12 "order-async-"
# 典型「任务不执行」的栈形态:大量线程停在队列取任务上,说明是任务处理慢而非线程不够
"order-async-3" #12 prio=5 os_prio=0 tid=0x... nid=0x... waiting on condition [0x...]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
# ② arthas 看线程池内部状态与最忙线程
thread --all | grep order-async
thread -n 3 # 查看 CPU 占用最高的 3 个线程,直接看到业务方法名
9.4 三个最容易埋雷的写法
**第一个是父子任务提交到同一个池(死锁)。**父任务占满所有线程并等待子任务结果,而子任务还在队列里排队等线程——双方互等,池彻底僵住:
// 反例:父任务占满线程后阻塞等待子任务
ExecutorService pool = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), new NamedThreadFactory("batch-"));
Future<Result> f = pool.submit(() -> {
Future<Integer> inner = pool.submit(() -> queryRemote()); // 队列里等线程
return inner.get(3, TimeUnit.SECONDS); // 父任务占着线程等
});
// 4 个父任务同时进来 → 所有线程都在等子任务,子任务永远拿不到线程 → 超时或永久阻塞
// 修法:子任务用独立的池,或改用 ForkJoinPool / CompletableFuture 的异步编排
**第二个是任务里没有超时。**一次不设超时的 HTTP 调用、一次没有 socketTimeout 的数据库查询,就能把线程池里的线程逐个钉死。所有外部依赖调用都必须有超时,并且超时值要小于上游的等待时间。
**第三个是ThreadLocal 与 MDC 没清理。**线程被复用意味着「上一个任务留下的上下文」会跟着进入下一个任务:
// 反例:ThreadLocal 未清理 → 线程复用后串数据,且强引用导致内存泄漏
pool.execute(() -> {
UserContext.set(currentUser); // 设置上下文
doBusiness(); // 途中抛异常,下面这行不执行
UserContext.remove(); // 永远执行不到
});
// 修法 1:任务内用 try-finally 包住,确保 remove()
// 修法 2(推荐):覆写 beforeExecute / afterExecute 统一清理
public class ContextAwarePool extends ThreadPoolExecutor {
@Override protected void beforeExecute(Thread t, Runnable r) { MDC.clear(); }
@Override protected void afterExecute(Runnable r, Throwable t) {
MDC.clear(); UserContext.remove(); // 无论成功失败都会执行
}
}
**还有一个隐蔽陷阱:**线程池的并发上限不等于系统的并发上限。如果多个池共享同一批下游资源(数据库连接池 20、Redis 连接池 50),即便每个池都只开 10 个线程,多池叠加也会把下游连接池打满,表现为「线程池很闲,但任务都在等连接」。所以容量规划要沿调用链一起看。
10. 面试速查表:十个高频问题
| 问题 | 关键回答 |
|---|---|
| 七大参数分别是什么? | core、max、keepAliveTime、unit、workQueue、threadFactory、handler |
| 任务提交的顺序是怎样的? | core 没满 → 建核心线程;否则入队;队列满 → 扩到 max;仍不行 → 拒绝 |
| 为什么线程数到 core 后先入队? | 优先复用已有线程,减少线程创建销毁开销;也避免线程数频繁抖动 |
| 无界队列有什么问题? | 队列永远不满,走不到扩容分支,max 失效;任务无限堆积直至 OOM |
| 线程是怎么复用的? | Worker 实现 Runnable,runWorker 里 while 循环调 getTask() 反复取任务 |
| 核心线程会销毁吗? | 会:allowCoreThreadTimeOut(true)、任务抛异常导致线程异常退出、池关闭 |
| shutdown 与 shutdownNow 的区别? | 前者中断空闲线程并处理完队列;后者中断所有线程、丢弃队列并返回未执行任务 |
| 为什么要用 CallerRunsPolicy? | 提供天然背压,让上游变慢而不是把任务丢掉;注意会占用调用方线程 |
| 线程数怎么定? | CPU 型 N+1;IO 型 N×(1+W/C) 常见 2N~2N+1;再用压测与监控收敛 |
| 为什么阿里禁止用 Executors? | 无界队列或无限线程会导致 OOM;参数语义被隐藏,事故难以发现 |
11. 小结
把线程池用对,靠的不是背参数,而是记住三条主线:
- **顺序决定行为。**core → 队列 → max → 拒绝,这意味着队列容量其实在控制「线程池愿不愿意扩容」。想让线程数弹性起来,队列必须小且有界。
- **一切过载都要有出口。**线程池把过载转换成队列积压,队列把过载转换成拒绝,拒绝必须能被观测到。缺了监控,线程池就变成了「静默丢任务的黑洞」。
- **线程池是隔离工具,而不是万能加速器。**不同业务分池、下游容量对齐、超时与降级配套,才是把线程池真正用稳的做法。
如果只带走一句话:先把队列改成有界、把拒绝策略接上监控、把线程名改成可读的业务名——这三件事做完,你已经能拦住绝大多数线上线程池事故了。