第 39 章:线程池
学习目标
- 理解为什么必须用线程池
- 掌握七大核心参数
- 学会参数调优与拒绝策略
一、为什么必须用线程池?
// ❌ 每来一个请求 new 一个线程
public void handleRequest() {
new Thread(() -> process()).start();
}三个致命问题:
| 问题 | 说明 |
|---|---|
| 创建销毁开销大 | 创建线程要调操作系统,约 1ms + 1MB 栈内存 |
| 无法限流 | 请求暴涨时线程数无上限 → OOM |
| 无法管理 | 拿不到执行状态,无法监控、无法优雅关闭 |
阿里规约:线程资源必须通过线程池提供,不允许显式创建线程。
二、七大核心参数
public ThreadPoolExecutor(
int corePoolSize, // ① 核心线程数
int maximumPoolSize, // ② 最大线程数
long keepAliveTime, // ③ 空闲线程存活时间
TimeUnit unit, // ④ 时间单位
BlockingQueue<Runnable> workQueue, // ⑤ 任务队列
ThreadFactory threadFactory, // ⑥ 线程工厂
RejectedExecutionHandler handler // ⑦ 拒绝策略
)任务提交流程(面试必考)
关键理解:队列满了才会创建非核心线程。所以如果你用了无界队列(
LinkedBlockingQueue默认容量Integer.MAX_VALUE),maximumPoolSize永远不会生效。
三、任务队列选择
| 队列 | 特点 | 适用 |
|---|---|---|
ArrayBlockingQueue | 有界,数组 | 推荐,能防 OOM |
LinkedBlockingQueue | 默认无界,链表 | ⚠️ 必须传容量参数 |
SynchronousQueue | 不存储,直接交给线程 | 任务少但要快速响应 |
PriorityBlockingQueue | 优先级排序 | 任务有优先级 |
DelayQueue | 延迟执行 | 定时任务 |
四、拒绝策略
// ① AbortPolicy(默认):抛 RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy();
// ② CallerRunsPolicy:让提交任务的线程自己执行 —— 天然限流,推荐
new ThreadPoolExecutor.CallerRunsPolicy();
// ③ DiscardPolicy:静默丢弃 —— 危险,任务丢了都不知道
new ThreadPoolExecutor.DiscardPolicy();
// ④ DiscardOldestPolicy:丢掉队列最老的任务,再重试
new ThreadPoolExecutor.DiscardOldestPolicy();
// ⑤ 自定义:记日志 + 落库补偿
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.error("线程池已满,任务被拒绝。活跃={}, 队列={}",
executor.getActiveCount(), executor.getQueue().size());
// 落库、发告警、写 MQ 补偿...
}
};推荐
CallerRunsPolicy:调用方线程去执行任务会变慢,自然减缓了提交速度,形成背压,比直接丢任务安全。
五、为什么不用 Executors?
// ❌ 阿里规约明确禁止
Executors.newFixedThreadPool(10); // 队列无界 → 任务堆积 OOM
Executors.newCachedThreadPool(); // 最大线程数 Integer.MAX_VALUE → 线程爆炸 OOM
Executors.newSingleThreadExecutor(); // 队列无界 → OOM
Executors.newScheduledThreadPool(1); // 队列无界 → OOM正确做法:手动 new
public class ThreadPoolConfig {
public static ThreadPoolExecutor create() {
return new ThreadPoolExecutor(
8, // ① 核心线程
16, // ② 最大线程
60L, TimeUnit.SECONDS, // ③ 空闲 60 秒回收
new ArrayBlockingQueue<>(1000), // ④ 有界队列,防 OOM
new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d") // ⑤ 命名!排查问题的关键
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // ⑥ 背压
);
}
}⑤ 线程命名有多重要? 线上出问题看线程 dump,
pool-1-thread-5你根本不知道是哪个业务;order-pool-5一眼定位。
六、参数怎么定?
CPU 密集型(计算为主)
corePoolSize = CPU 核数 + 1线程数超过核数只会增加上下文切换开销。+1 是为了在某线程偶尔阻塞时仍能跑满 CPU。
IO 密集型(数据库、网络调用为主)
corePoolSize = CPU 核数 × (1 + 平均等待时间 / 平均计算时间)实际举例:8 核机器,接口平均耗时 100ms,其中 CPU 计算 5ms,等数据库 95ms:
8 × (1 + 95/5) = 8 × 20 = 160⚠️ 公式只是起点。真实值必须靠压测确定:逐步加大线程数,观察 QPS 和 RT,找到拐点。
int cpuCores = Runtime.getRuntime().availableProcessors();七、提交任务:execute vs submit
// execute:无返回值,异常直接抛到 UncaughtExceptionHandler
pool.execute(() -> doWork());
// submit:返回 Future,异常被封装在 Future 里
Future<String> future = pool.submit(() -> "结果");
String result = future.get(); // ① 阻塞直到有结果
String r2 = future.get(3, TimeUnit.SECONDS); // ② 超时控制,推荐⚠️ submit 的隐形陷阱:任务抛异常时不会打印任何日志,异常被藏在
Future里。如果你不调get(),异常就永远消失了。 对策:任务内部自己 try-catch,或者一定要调get()。
八、优雅关闭
public void shutdownGracefully(ExecutorService pool) {
pool.shutdown(); // ① 不再接受新任务,已提交的继续执行
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { // ② 等 60 秒
pool.shutdownNow(); // ③ 强制中断
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
log.error("线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt(); // ④ 恢复中断
}
}| 方法 | 行为 |
|---|---|
shutdown() | 温和:不收新任务,跑完存量 |
shutdownNow() | 强制:中断运行中的任务,返回未执行的任务列表 |
awaitTermination(t, unit) | 阻塞等待关闭完成 |
九、Spring Boot 中的线程池
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("bizExecutor")
public ThreadPoolTaskExecutor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("biz-async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
// ① 优雅关闭:应用停止时等待任务执行完
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
// 使用
@Async("bizExecutor") // ② 指定线程池名,别用默认的
public void sendEmail(String to) { }⚠️
@Async不指定线程池名时,Spring Boot 用SimpleAsyncTaskExecutor——它每次都新建线程,根本不是池!
十、监控线程池
// 定时上报关键指标
public void monitor(ThreadPoolExecutor pool) {
log.info("核心={}, 活跃={}, 最大={}, 队列={}/{}, 已完成={}",
pool.getCorePoolSize(),
pool.getActiveCount(), // ① 正在执行任务的线程数
pool.getMaximumPoolSize(),
pool.getQueue().size(), // ② 排队任务数 —— 最重要的指标
pool.getQueue().size() + pool.getQueue().remainingCapacity(),
pool.getCompletedTaskCount());
}告警规则:队列使用率 > 80% 时告警。这说明消费速度跟不上生产速度,再涨就要触发拒绝策略了。
十一、本章小结
| 要点 | 关键 |
|---|---|
| 必须用线程池 | 复用、限流、可管理 |
| 提交流程 | 核心 → 队列 → 非核心 → 拒绝 |
| 无界队列 | maxPoolSize 失效,会 OOM |
| 拒绝策略 | 推荐 CallerRunsPolicy |
| 禁用 Executors | 一律手动 new |
| 线程命名 | 必须,排查问题靠它 |
@Async | 必须指定自定义线程池 |
| 优雅关闭 | shutdown + awaitTermination |
动手练习
练习 1:基础题
创建一个 core=2, max=4, queue=2 的线程池,提交 10 个任务(每个 sleep 1 秒),观察哪些被执行、哪些被拒绝,验证提交流程。
练习 2:进阶题
实现一个可动态调整参数的线程池:暴露 HTTP 接口,运行时修改 corePoolSize 和 maximumPoolSize(提示:setCorePoolSize() 方法支持运行时修改)。
下一章:第 40 章:JUC 与 JVM →