线程池设计
1. 这是什么
线程池用于复用线程资源、限制并发度、减少线程创建销毁成本。
它是 Java 并发开发里最常见也最容易出问题的工具之一。
一句话理解:
- 线程池不是“让代码变异步”的小工具
- 它更像“线程资源配额中心”
2. 为什么重要
线程池参数配置不当,轻则吞吐下降,重则把系统、数据库、缓存、下游接口一起拖垮。
高级开发者需要理解:
- 线程池是容量控制工具
- 也是系统保护机制
如果线程池设计不好,常见后果包括:
- 任务无限堆积
- 请求排队时间暴涨
- 线程数过高导致上下文切换激增
- 下游资源被并发打爆
3. 先建立直觉:线程池到底在管理什么
线程池本质上在管理三件事:
| 维度 | 问题 |
|---|---|
| 线程数量 | 同时允许多少任务并发执行 |
| 排队容量 | 暂时执行不过来时,允许积压多少任务 |
| 拒绝策略 | 当线程和队列都撑满时怎么办 |
所以线程池从来不只是:
- “开几个线程”
而是完整的资源调度和保护设计。
4. 核心内容
4.1 ThreadPoolExecutor 的关键参数
最重要的几个参数是:
corePoolSizemaximumPoolSizekeepAliveTimeworkQueuethreadFactoryRejectedExecutionHandler
4.2 核心线程数和最大线程数怎么配合
可以先用这个顺序理解任务进入线程池后的路径:
- 先看核心线程是否还没满
- 没满就优先创建核心线程执行
- 核心线程满了,任务先尝试入队
- 队列也满了,再尝试扩到最大线程数
- 如果最大线程数也满了,就走拒绝策略
这条路径非常关键,因为很多线程池问题都源于对它理解不清。
4.3 队列容量为什么重要
队列并不是“越大越安全”。
队列太大可能导致:
- 任务大量积压
- 请求延迟变长
- 问题被隐藏得更深
- 内存压力变大
队列太小则可能导致:
- 拒绝更早发生
- 波峰期更容易触发降级
真正的关键是:
- 队列大小要和业务可接受延迟、下游容量、失败策略一起设计
4.4 线程工厂为什么不能忽略
自定义 ThreadFactory 的常见价值:
- 给线程命名,方便排障
- 设置是否守护线程
- 统一设置优先级或上下文
线上排查线程池问题时,线程名非常重要。
如果全是默认名字,定位成本会明显增加。
4.5 拒绝策略为什么是系统保护机制
线程池满了以后,必须有明确策略。
常见拒绝策略包括:
- 直接抛异常
- 由调用线程执行
- 丢弃任务
- 丢弃最旧任务
它们没有绝对好坏,重点在于:
- 你的系统希望“怎么失败”
这就是线程池为什么本质上也是系统保护机制。
4.6 CPU 密集型和 I/O 密集型任务的差异
CPU 密集型
特点:
- 任务主要消耗 CPU
- 线程太多会导致大量上下文切换
通常思路:
- 线程数接近 CPU 核数或略高
I/O 密集型
特点:
- 任务大量时间在等待 I/O
- 线程可以适当多一些
通常思路:
- 线程数可比 CPU 核数高,但不能无限加
4.7 为什么不建议直接使用默认线程池工厂
Executors 提供的某些默认工厂虽然方便,但在生产环境里容易埋坑,例如:
- 队列过大或无界
- 最大线程数过高
- 缺少业务可观测配置
工程上更推荐:
- 显式使用
ThreadPoolExecutor - 明确核心参数、线程名和拒绝策略
5. 学习重点
这一章最重要的是建立这些判断:
- 线程数不是越大越好
- 队列不是越大越安全
- 拒绝策略不是兜底细节,而是架构选择
- 不同类型任务应该尽量分开线程池
- 线程池本质上承担容量控制和隔离职责
6. 常见问题
6.1 直接使用默认线程池工厂
方便不等于适合生产。
你需要的是可控,而不是省两行代码。
6.2 任务无限堆积却没有监控
线程池如果只有“能跑”而没有:
- 活跃线程监控
- 队列长度监控
- 拒绝次数监控
问题往往要等到系统变慢后才暴露。
6.3 一个线程池承载完全不同类型的任务
例如把:
- 核心接口任务
- 慢 I/O 任务
- 批量后台任务
全塞到一个池里,会导致相互拖累。
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 ThreadPoolDesignDemo.java,内容如下:
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolDesignDemo {
public static void main(String[] args) throws Exception {
AtomicInteger threadId = new AtomicInteger(0);
AtomicInteger rejected = new AtomicInteger(0);
AtomicInteger active = new AtomicInteger(0);
AtomicInteger maxActive = new AtomicInteger(0);
CountDownLatch release = new CountDownLatch(1);
CopyOnWriteArrayList<String> threadNames = new CopyOnWriteArrayList<>();
ThreadFactory threadFactory = task -> {
Thread thread = new Thread(task);
thread.setName("biz-pool-" + threadId.incrementAndGet());
return thread;
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
threadFactory,
(task, pool) -> rejected.incrementAndGet());
Runnable task = () -> {
threadNames.add(Thread.currentThread().getName());
int current = active.incrementAndGet();
maxActive.updateAndGet(old -> Math.max(old, current));
try {
release.await();
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
active.decrementAndGet();
}
};
executor.execute(task);
executor.execute(task);
executor.execute(task);
executor.execute(task);
Thread.sleep(200);
System.out.println("corePoolSize=" + executor.getCorePoolSize());
System.out.println("maximumPoolSize=" + executor.getMaximumPoolSize());
System.out.println("largestPoolSize=" + executor.getLargestPoolSize());
System.out.println("queueSizeBeforeRelease=" + executor.getQueue().size());
System.out.println("rejectedCount=" + rejected.get());
System.out.println("threadNamePrefixOk=" + threadNames.stream().allMatch(name -> name.startsWith("biz-pool-")));
release.countDown();
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("completedTaskCount=" + executor.getCompletedTaskCount());
System.out.println("maxActiveThreads=" + maxActive.get());
}
}7.2 编译并运行
bash
javac ThreadPoolDesignDemo.java
java ThreadPoolDesignDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
text
corePoolSize=1
maximumPoolSize=2
largestPoolSize=2
queueSizeBeforeRelease=1
rejectedCount=1
threadNamePrefixOk=true
completedTaskCount=3
maxActiveThreads=27.4 每一行在验证什么
corePoolSize=1、maximumPoolSize=2:说明这个线程池的容量边界是明确配置的largestPoolSize=2:说明线程池确实扩到了最大线程数queueSizeBeforeRelease=1:说明核心线程满后,任务会先进入队列rejectedCount=1:说明线程和队列都满时,会触发拒绝策略threadNamePrefixOk=true:说明自定义线程工厂让线程更可观测completedTaskCount=3:说明 4 个任务里有 3 个被接收执行,1 个被拒绝maxActiveThreads=2:说明同时执行的任务数受最大线程数限制
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 把队列容量从
1改成10 - 把拒绝策略改成调用线程执行
你可以观察:
- 队列变大后,拒绝可能变少,但排队延迟会增加
- 不同拒绝策略会改变系统在超载时的行为
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 为不同业务场景设计不同线程池参数
- 对比 CPU 密集型和 I/O 密集型任务的线程池配置思路
- 观察不同队列容量和拒绝策略下的运行结果
- 总结线程池核心参数之间的协作关系
9. 自测问题
- 为什么不建议直接使用默认线程池工厂?
- 核心线程数、最大线程数、队列容量如何共同作用?
- 线程池为什么也是系统保护机制的一部分?
- 为什么不同类型任务不应该随意共用一个线程池?
- 自定义线程工厂为什么重要?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 线程池本质上是线程资源复用和容量控制工具
- 核心线程、最大线程、队列和拒绝策略共同决定系统行为
- 线程数和队列容量都不是越大越好
- 线程池应该具备清晰命名、监控和隔离思维
- 线程池设计直接关系到系统在高负载下的稳定性