线程池学习总览
线程池不是“让代码异步一下”的工具,而是 Java 后端系统控制并发、保护资源、隔离故障的核心组件。接口突然变慢、请求堆积、CPU 飙高、内存上涨、数据库连接池打满、MQ 消费延迟,很多线上问题最后都能追到线程池配置和使用方式。
本目录把线程池从多线程中独立出来,按原理、参数、执行流程、队列、拒绝策略、关闭、监控和排查完整学习。
学习目标
学完线程池专题,要能回答:
- 为什么不能每个任务都
new Thread()。 Executor、ExecutorService、ThreadPoolExecutor有什么关系。- 七大核心参数分别控制什么。
- 线程池为什么是“核心线程 -> 队列 -> 最大线程 -> 拒绝策略”的流程。
- 不同阻塞队列会如何改变线程池行为。
- 为什么生产不推荐直接使用
Executors。 - CPU 密集型、IO 密集型、下游受限任务如何估算参数。
- 线程池打满、任务堆积、线程饥饿死锁如何排查。
线程池解决什么问题
如果每来一个任务都创建一个线程:
mermaid
flowchart TD
A["请求到来"] --> B["new Thread"]
B --> C["执行业务任务"]
C --> D["线程结束销毁"]
A --> E["高并发时大量创建线程"]
E --> F["上下文切换增加"]
E --> G["线程栈占用内存"]
E --> H["下游数据库或接口被打爆"]短期看代码简单,长期会出现三个问题:
| 问题 | 原因 | 后果 |
|---|---|---|
| 创建销毁成本高 | 线程是操作系统资源,不是普通对象 | 高并发时 CPU 浪费在调度上 |
| 数量不可控 | 请求越多线程越多 | 内存上涨,甚至无法创建新线程 |
| 缺少排队和拒绝 | 系统没有容量边界 | 无限堆积直到 OOM 或整体雪崩 |
线程池的本质是:用有限线程处理大量任务,用队列吸收短期峰值,用拒绝策略在系统过载时保护自己。
线程池核心结构
mermaid
flowchart TD
A["业务线程提交任务"] --> B["ThreadPoolExecutor"]
B --> C["Worker 工作线程集合"]
B --> D["BlockingQueue 任务队列"]
B --> E["RejectedExecutionHandler 拒绝策略"]
B --> F["ThreadFactory 线程工厂"]
C --> G["执行 Runnable 或 Callable"]
D --> C核心组件:
| 组件 | 作用 |
|---|---|
ThreadPoolExecutor | 线程池核心实现,控制线程创建、队列、拒绝、关闭 |
Worker | 线程池内部工作线程,循环取任务执行 |
BlockingQueue | 保存等待执行的任务 |
ThreadFactory | 创建线程并设置名称、是否守护线程、异常处理 |
RejectedExecutionHandler | 线程池满了以后决定任务怎么处理 |
学习顺序
mermaid
flowchart TD
A["线程池为什么存在"] --> B["七大核心参数"]
B --> C["execute 执行流程"]
C --> D["阻塞队列和拒绝策略"]
D --> E["参数估算和线程池隔离"]
E --> F["关闭流程和任务异常"]
F --> G["监控指标和线上排查"]推荐按下面顺序学习:
- ExecutorService:已有基础页,理解接口和基本使用。
- 核心参数:把七大参数逐个讲透。
- 执行流程原理:理解任务提交后到底怎么流转。
- 队列与拒绝策略:理解队列选择如何改变线程池行为。
- 参数估算与监控:理解商业项目怎么配、怎么观测。
- 源码级生命周期与线上排查:把
execute、Worker、getTask、线程状态、队列堆积和jstack排查串起来。 - 关闭流程与常见坑:理解优雅停机、异常吞掉、饥饿死锁。
- Callable、Future与FutureTask:理解有返回值任务和异常获取。
最小可运行 Demo
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolQuickStart {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
4,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("order-worker-" + thread.getId());
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 20; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 处理订单任务 " + taskId);
});
}
pool.shutdown();
}
}这个 Demo 有几个重点:
- 使用有界队列
ArrayBlockingQueue,避免任务无限堆积。 - 自定义线程名,线上排查
jstack时能看出线程用途。 - 使用
CallerRunsPolicy,线程池满了让提交线程自己执行,形成反压。 - 最后调用
shutdown(),避免应用无法退出。
为什么不推荐直接用 Executors
Executors 写起来方便,但很多工厂方法隐藏了风险。
| 方法 | 隐藏风险 |
|---|---|
newFixedThreadPool | 默认使用无界 LinkedBlockingQueue,任务可能无限堆积 |
newSingleThreadExecutor | 也是无界队列,单线程卡住会导致后续任务全部等待 |
newCachedThreadPool | 使用 SynchronousQueue,最大线程数接近无限,流量高时线程数失控 |
newScheduledThreadPool | 周期任务异常可能停止后续执行,需要捕获异常 |
生产环境更推荐显式创建 ThreadPoolExecutor,把线程数、队列长度、线程名、拒绝策略都写清楚。
商业常用场景
| 场景 | 线程池作用 | 配置重点 |
|---|---|---|
| 接口聚合 | 并行调用多个下游接口 | 超时、隔离、最大并发 |
| 批量采集 | 控制采集并发,避免打爆目标系统 | 队列长度、限流、重试 |
| 异步入库 | 削峰,把写入任务排队处理 | 有界队列、失败补偿 |
| MQ 消费 | 控制消费速度和业务处理并发 | 幂等、重试、消费堆积 |
| 报表导出 | 避免大任务占满业务线程 | 独立线程池、任务状态 |
面试标准回答
问题:线程池有什么作用?
标准回答:
线程池用于复用线程、控制并发数量、管理任务队列和拒绝策略。它避免每个任务都创建新线程带来的资源浪费,也能在流量高峰时通过队列缓冲任务,在系统过载时通过拒绝策略保护服务。生产中线程池必须结合业务类型、下游容量、队列长度、超时和监控来配置,不能无脑使用 Executors。
本章小结
线程池是并发控制工具,不是无限加速工具。理解线程池要抓住四件事:线程数量有限、队列容量有限、下游容量有限、过载时必须有拒绝或反压。后续章节会逐步拆解参数、执行流程、队列、拒绝策略、生命周期、监控和排查。
