线程池

为什么用线程池

不用线程池的痛点:每次任务来了就 new Thread(),创建 / 销毁线程开销大(涉及系统资源),且线程数不可控,大量并发会撑爆内存。

线程池的好处:复用线程、控制最大并发数、统一管理任务排队,避免频繁创建销毁。

线程池怎么使用

首先最常用的是通过 Executors 工具类快速创建线程池,适合简单场景,不用手动配置复杂参数。

为什么不建议用 Executors 提供的快捷方法

Executors有newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool 等,问题在于它们默认的队列:

  • newFixedThreadPool / newSingleThreadExecutor → 用的是无界队列 LinkedBlockingQueue(默认 Integer.MAX_VALUE)。任务无限堆积,可能内存耗尽 / OOM。
  • newCachedThreadPool → 核心线程 0,最大线程 Integer.MAX_VALUE,可创建无限线程,可能创建过多线程耗尽资源。
  • newScheduledThreadPool → 同上,最大线程上限极大。

标准答法:尽量用 new ThreadPoolExecutor 手动指定有界队列 + 拒绝策略,避免无界队列和无限线程带来的 OOM 风险。 这也是阿里开发规范明确建议的。

七大核心参数

ThreadPoolExecutor 构造:

1
2
3
4
5
6
7
8
9
new ThreadPoolExecutor(
corePoolSize, // 1. 核心线程数:常驻,即使空闲也不销毁
maximumPoolSize, // 2. 最大线程数:核心 + 非核心的上限
keepAliveTime, // 3. 空闲存活时间:非核心线程空闲超过它会被回收
TimeUnit.SECONDS, // 4. 存活时间的单位
workQueue, // 5. 任务队列:核心线程满了,任务先进这里排队
threadFactory, // 6. 线程工厂:创建线程,可命名/设优先级
handler // 7. 拒绝策略:队列也满了怎么处理新任务
);

提交任务后的执行流程

任务进来,按下面的顺序判断:

  1. 核心线程没满(当前线程数 < corePoolSize)→ 直接创建核心线程执行。
  2. 核心线程满了 → 任务放入 workQueue 排队。
  3. 队列也满了 → 创建非核心线程执行(线程数可以到 maximumPoolSize)。
  4. 线程数已经到 maximumPoolSize,队列也满 → 走拒绝策略。

四种拒绝策略

策略 行为 适用
AbortPolicy(默认) 直接抛 RejectedExecutionException 任务不能丢,希望快速失败暴露问题
CallerRunsPolicy 不丢弃,由提交任务的线程自己执行 慢速降级、保证任务不丢
DiscardPolicy 静默丢弃新任务 允许丢、可接受丢失
DiscardOldestPolicy 丢弃队列里最老的任务,再尝试提交新的 追求新任务优先

合理配置线程数

  • CPU 密集型(大量计算,不吃 IO):线程数 ≈ CPU 核数 + 1(或核数),避免过多线程切换反而变慢。
  • IO 密集型(等待磁盘 / 网络):线程数 ≈ CPU 核数 × 目标 CPU 利用率 × (1 + 等待时间 / 计算时间)。

线程池异常怎么处理

execute() 提交: 如果任务抛出未捕获的运行时异常,该线程会直接崩溃。异常信息会打印到控制台(或者错误日志),然后线程池会销毁这个旧线程,并创建一个新线程来顶替它。

submit() 提交: 线程池会把任务包装成一个 Future 对象。即使任务内部抛出异常,线程也不会崩溃,异常也不会打印。异常被偷偷“藏”在了 Future 里面,直到你调用 future.get() 时,它才会被重新抛出。

处理方式:

1.execute 提交 + 任务内 try-catch:在 Runnable 里自己捕获。

1
2
3
4
5
6
7
8
9
10
executor.execute(() -> {
try {
// 你的核心业务逻辑
int result = 10 / 0;
} catch (Exception e) {
// 1. 记录日志
log.error("任务执行失败", e);
// 2. 触发报警机制、写入重试表等补偿逻辑
}
});

2.submit 提交 + Future.get ():用 Future 拿结果,get() 会抛出 ExecutionException 包裹原始异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Future<?> future = executor.submit(() -> {
// 可能会抛出异常的业务
});

try {
future.get(); // 阻塞等待结果
} catch (InterruptedException e) {
// 线程被中断
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
// 任务内部抛出的异常会被封装在这里
Throwable realCause = e.getCause();
log.error("捕获到任务内部异常", realCause);
}