线程池执行流程原理
线程池最容易被误解的点是执行流程。很多人以为任务来了会优先创建到最大线程数,其实 ThreadPoolExecutor 的核心流程是:先核心线程,再队列,再最大线程,最后拒绝策略。
execute 总流程
flowchart TD
A["调用 execute 提交任务"] --> B{"运行线程数小于 corePoolSize"}
B -- "是" --> C["创建核心 Worker 执行任务"]
B -- "否" --> D{"线程池是否运行中"}
D -- "否" --> E["拒绝任务"]
D -- "是" --> F{"任务能否放入队列"}
F -- "是" --> G["任务进入 workQueue"]
G --> H["Worker 从队列取任务执行"]
F -- "否" --> I{"运行线程数小于 maximumPoolSize"}
I -- "是" --> J["创建非核心 Worker 执行任务"]
I -- "否" --> K["执行拒绝策略"]这个流程解释了一个非常重要的现象:如果你使用无界队列,队列永远不容易满,那么 maximumPoolSize 基本不会生效。
为什么流程这样设计
| 阶段 | 设计目的 | 如果没有这一层 |
|---|---|---|
| 核心线程 | 保留基础处理能力 | 每个任务都排队或临时建线程 |
| 队列 | 吸收短时间流量波动 | 流量稍微上涨就创建大量线程 |
| 最大线程 | 队列也扛不住时临时扩容 | 峰值只能排队,延迟上升 |
| 拒绝策略 | 系统满载时保护自己 | 无限堆积,拖垮 JVM 和下游 |
线程池不是为了让所有任务立刻执行,而是为了在吞吐、延迟、资源和稳定性之间做平衡。
Worker 是什么
Worker 可以理解为线程池里的工作单元:它内部持有一个线程,并负责执行第一个任务,然后循环从队列取后续任务。
flowchart TD
A["Worker 创建"] --> B["执行 firstTask"]
B --> C["进入 runWorker 循环"]
C --> D["从队列 getTask"]
D --> E{"是否取到任务"}
E -- "是" --> F["执行任务"]
F --> C
E -- "否" --> G["Worker 退出并回收"]所以线程池不是“一个任务一个线程”,而是“一个 Worker 线程不断消费多个任务”。
JDK 7/8 中要重点理解的点
线程池是 JDK 5 引入并在 JDK 7/8 中大量使用的核心并发工具。JDK 8 虽然增加了 CompletableFuture 和并行 Stream,但底层仍离不开线程池。
| 能力 | JDK 7 常见写法 | JDK 8 常见写法 |
|---|---|---|
| 无返回值异步 | ExecutorService.execute | 仍然可用 |
| 有返回值异步 | submit + Future | CompletableFuture 更方便组合 |
| 并行集合处理 | 手写线程池拆任务 | parallelStream,但要小心公共线程池 |
| 多任务聚合 | 多个 Future 手动 get | CompletableFuture.allOf |
不要以为 JDK 8 有了 CompletableFuture 就不需要线程池。CompletableFuture 只是任务编排工具,真正执行任务的还是线程池。
execute 和 submit 的区别
flowchart TD
A["提交任务"] --> B{"使用 execute"}
A --> C{"使用 submit"}
B --> D["只提交 Runnable"]
D --> E["异常直接进入线程异常处理"]
C --> F["包装成 FutureTask"]
F --> G["异常保存在 Future 中"]
G --> H["调用 get 时抛出 ExecutionException"]区别:
| 对比 | execute | submit |
|---|---|---|
| 返回值 | 无 | 返回 Future |
| 异常表现 | 任务异常可能直接打印或由异常处理器处理 | 异常被包装,调用 get() 才能看到 |
| 适合场景 | 不关心结果的任务 | 需要结果、取消、等待的任务 |
错误示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SubmitExceptionDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
throw new RuntimeException("任务失败");
});
pool.shutdown();
}
}这段代码可能不会在主线程直接看到异常,因为异常被保存在 Future 里了。
正确写法:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class SubmitExceptionRightDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<?> future = pool.submit(() -> {
throw new RuntimeException("任务失败");
});
try {
future.get();
} finally {
pool.shutdown();
}
}
}线程池状态如何影响执行
线程池不是只有“运行”和“停止”两个状态。
flowchart TD
A["RUNNING"] --> B["SHUTDOWN"]
A --> C["STOP"]
B --> D["TIDYING"]
C --> D
D --> E["TERMINATED"]| 状态 | 含义 |
|---|---|
RUNNING | 接收新任务,也处理队列任务 |
SHUTDOWN | 不接收新任务,但继续处理队列任务 |
STOP | 不接收新任务,不处理队列任务,并中断正在执行的任务 |
TIDYING | 所有任务结束,准备执行终止钩子 |
TERMINATED | 完全终止 |
如果调用 shutdown() 后还继续提交任务,就会触发拒绝策略。
商业场景:接口聚合
订单详情页可能需要并行查询订单、支付、物流、优惠券。线程池执行流程决定了接口在高峰期会怎么表现。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class OrderQueryPoolDemo {
private static final ThreadPoolExecutor QUERY_POOL = new ThreadPoolExecutor(
8,
16,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200),
r -> new Thread(r, "order-query-" + System.nanoTime()),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public static void main(String[] args) {
QUERY_POOL.execute(() -> System.out.println("查询订单"));
QUERY_POOL.execute(() -> System.out.println("查询支付"));
QUERY_POOL.execute(() -> System.out.println("查询物流"));
QUERY_POOL.shutdown();
}
}如果下游支付接口很慢,线程会被占住。此时队列会积压,最后触发拒绝或反压。真正的生产配置还要加超时、熔断、限流和下游隔离。
面试标准回答
问题:线程池任务提交后的执行流程是什么?
标准回答:
任务通过 execute 提交后,线程池会先判断当前工作线程数是否小于核心线程数,如果小于就创建核心线程执行;否则尝试把任务放入阻塞队列;如果队列满了,再判断是否能创建到最大线程数;如果最大线程数也满了,就执行拒绝策略。这个流程说明线程池不是优先创建最大线程,而是先使用核心线程和队列来稳定处理任务。
本章小结
线程池执行流程要和参数一起理解。无界队列会让最大线程数失效,submit 会隐藏任务异常,shutdown 后继续提交会触发拒绝。真正掌握线程池,必须知道任务从提交、入队、执行、异常、关闭到回收的完整链路。
