线程池ThreadPoolExecutor

一、七大核心参数

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)

逐个通俗解释

  1. corePoolSize 核心线程数

    常驻线程,不会回收,一直在线程池里待命;

    即使空闲,也不会被销毁。

  2. maximumPoolSize 最大线程数

    线程池能容纳的总线程上限

    非核心线程 = 最大线程数 - 核心线程数。

  3. keepAliveTime 空闲存活时间

    只针对非核心线程

    空闲超过该时间,自动回收,节省资源。

  4. workQueue 任务阻塞队列

    核心线程满了,新任务进入队列排队;

    常用:LinkedBlockingQueueArrayBlockingQueueSynchronousQueue

  5. threadFactory 线程工厂

    统一创建线程、设置线程名、优先级、是否守护线程。

  6. handler 拒绝策略

    核心线程满 + 队列满 + 最大线程也满 → 触发拒绝。


二、线程池标准执行流程

  1. 提交任务
  2. 核心线程数未满 → 新建核心线程执行任务
  3. 核心线程已满 → 任务进入 阻塞队列 排队
  4. 队列已满 → 新建非核心线程执行任务
  5. 线程总数达到 maximumPoolSize → 触发拒绝策略

顺序牢记:核心线程 → 队列 → 非核心线程 → 拒绝


三、四种拒绝策略(JDK 内置)

  1. AbortPolicy(默认)

    直接抛出 RejectedExecutionException,中断业务。

  2. CallerRunsPolicy

    不抛异常、不丢弃;

    交给提交任务的主线程自己执行,限速限流。

  3. DiscardPolicy

    默默丢弃当前新任务,无异常、无日志,容易丢数据。

  4. DiscardOldestPolicy

    丢弃队列最旧的任务,塞入当前新任务。


四、线程池 5 种状态 & 转换

5 种状态

  1. RUNNING:正常运行,接收新任务、处理队列任务
  2. SHUTDOWN:不接收新任务,继续执行队列剩余任务
  3. STOP:不接收新任务、中断正在执行的任务、清空队列
  4. TIDYING:所有任务结束,线程数为 0,准备收尾
  5. TERMINATED:线程池彻底关闭

状态流转

  • RUNNING → shutdown() → SHUTDOWN
  • RUNNING → shutdownNow() → STOP
  • SHUTDOWN/STOP 任务跑完 → TIDYING → TERMINATED

底层存储

线程池用一个 AtomicInteger 打包:

高 3 位:线程池状态 | 低 29 位:当前线程数量

通过 CAS 原子修改,保证并发安全。


五、为什么禁止使用 Executors 快速创建线程池

阿里开发手册强制禁止,本质:资源不可控,极易引发线上故障

1. Executors 弊端

  • Executors.newFixedThreadPool()

    无界 LinkedBlockingQueue,任务无限堆积 → OOM

  • Executors.newSingleThreadExecutor()

    同样无界队列,积压打爆内存

  • Executors.newCachedThreadPool()

    最大线程数 Integer.MAX_VALUE → 极端情况无限创建线程,CPU 打满、OOM

  • 无自定义拒绝策略,异常不可控

  • 线程工厂默认,线程无意义名称,线上排查困难

2. 正确做法

手动 new ThreadPoolExecutor() 构造器:

  • 手动指定有界队列

  • 限制最大线程数

  • 自定义拒绝策略(降级 / 告警 / 落地)

    告警:

    任务满了被拒绝 → 打日志、发监控、发短信 / 钉钉告警

    作用:告诉运维 / 开发:系统压力炸了,有任务处理不过来。

    降级:

    任务满了被拒绝 → 放弃复杂逻辑,走简单兜底逻辑

    落地(持久化):

    任务实在处理不下、既不能降级、也不能丢 →

    把任务数据 存数据库 / 存本地文件 / 丢到 MQ / 写入 Redis

  • 自定义线程名,方便问题排查


六、补充关键细节

  1. 核心线程默认不超时回收,可通过 allowCoreThreadTimeOut 开启回收
  2. 非核心线程靠 keepAliveTime 自动销毁
  3. 队列一定要有界,是防止 OOM 最关键手段
  4. 生产环境拒绝策略不能用默认抛异常,要做业务降级、MQ 重试、日志落库

极简汇总

  1. 七大参数:核心线程、最大线程、空闲超时、阻塞队列、线程工厂、拒绝策略。
  2. 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝。
  3. 拒绝策略:抛异常、调用者执行、丢弃、丢弃最旧。
  4. 五种状态:RUNNING / SHUTDOWN / STOP / TIDYING / TERMINATED。
  5. 禁用 Executors:无界队列 / 无限线程,导致 OOM、CPU 飙高;生产手动创建。

execute()与submit()区别

核心定位

execute

void execute(Runnable command)

  • 无返回值
  • 只负责执行任务

submit

<T> Future<T> submit(xxx)

  • 有返回值,返回 Future 对象
  • 支持传:Runnable / Runnable + 结果 / Callable

五大核心区别

1. 返回值不同

  • execute:无返回值
  • submit:返回 Future,可以获取任务执行结果、取消任务

2. 异常处理完全不同

execute

  • 任务抛出异常 → 直接往外抛

  • 若无捕获 + 无全局异常处理器:

    线程池会销毁当前工作线程、新建替补线程

  • 控制台 / 日志能直接看到异常堆栈(前提:没被线程池吞掉)

submit

  • 任务异常不会直接抛出
  • 异常会被封装进 Future 内部
  • 不调用 future.get(),永远看不到异常,完全静默丢失
1
2
3
Future<?> future = pool.submit(() -> 1/0);
// 不get:全程无报错、无日志
// future.get() 才会抛出封装后的 ExecutionException

3. 支持的任务类型

  • execute:只支持 Runnable
  • submit:支持
    • Runnable
    • Runnable + 传入默认返回值
    • Callable(有返回值任务)

4. 底层实现

  • 两者最终都会调用同一个核心执行逻辑
  • submit 会把 Callable/Runnable 统一包装成 FutureTask

5. 任务取消、状态控制

  • execute:不能取消、不能感知执行状态
  • submit:通过 Future 可
    • get() 拿结果
    • cancel() 取消任务
    • isDone() 判断是否完成

线程池任务抛异常没日志、没提示、悄无声息挂掉

一、为什么没日志?

1. ThreadPoolExecutor 默认不打印任何异常堆栈

线程池执行任务时:

  • 如果任务里没 try-catch
  • 抛出了异常
  • 线程池只会 quietly 把这个线程销毁不打日志、不抛异常、不通知

你完全不知道任务挂了!

2. 两种提交方式区别

execute() → 异常直接吃掉 (最坑)

1
2
3
4
executor.execute(() -> {
// 异常!没日志!
int i = 1 / 0;
});

异常直接消失,控制台啥都没有!

submit() → 异常存在 Future 里,不打印

1
future.get(); // 必须调用 get() 才能看到异常

你不调用 get()永远看不到异常!

二、3 种解决方法

方法 1:任务内部 强制 try-catch(最简单、最稳)

所有提交给线程池的任务,必须包一层 try-catch

1
2
3
4
5
6
7
8
executor.execute(() -> {
try {
// 你的业务代码
} catch (Throwable t) {
// 强制打印日志!!
log.error("线程池任务执行异常", t);
}
});

优点

  • 100% 捕获异常
  • 日志必打
  • 不会丢错误

方法 2:自定义 ThreadFactory,给线程设置UncaughtExceptionHandler

让线程崩溃前强制打印日志

1
2
3
4
5
6
7
8
9
10
11
ThreadFactory factory = r -> {
Thread t = new Thread(r);
// 设置崩溃日志处理器
t.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("线程崩溃: " + thread.getName(), throwable);
});
return t;
};

// 构建线程池时传入
new ThreadPoolExecutor(..., factory, ...);

优点

  • 全局统一处理
  • 不用每个任务写 try-catch

方法 3:重写 ThreadPoolExecutor.afterExecute

线程池自带一个钩子方法

任务执行完(包括异常)一定会调用!

1
2
3
4
5
6
7
8
9
10
public class LogThreadPoolExecutor extends ThreadPoolExecutor {

@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
// 任务抛出异常,强制打日志
log.error("线程池任务执行异常", t);
}
}
}

终极方案

  • 全局捕获
  • 所有异常都跑不掉
  • 生产标准用法

线程池ThreadPoolExecutor
http://hanqichuan.com/2026/04/16/java并发/线程池ThreadPoolExecutor/
作者
韩启川
发布于
2026年4月16日
许可协议