线程池ThreadPoolExecutor
一、七大核心参数
1 | |
逐个通俗解释
corePoolSize 核心线程数
常驻线程,不会回收,一直在线程池里待命;
即使空闲,也不会被销毁。
maximumPoolSize 最大线程数
线程池能容纳的总线程上限;
非核心线程 = 最大线程数 - 核心线程数。
keepAliveTime 空闲存活时间
只针对非核心线程;
空闲超过该时间,自动回收,节省资源。
workQueue 任务阻塞队列
核心线程满了,新任务进入队列排队;
常用:
LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue。threadFactory 线程工厂
统一创建线程、设置线程名、优先级、是否守护线程。
handler 拒绝策略
核心线程满 + 队列满 + 最大线程也满 → 触发拒绝。
二、线程池标准执行流程
- 提交任务
- 核心线程数未满 → 新建核心线程执行任务
- 核心线程已满 → 任务进入 阻塞队列 排队
- 队列已满 → 新建非核心线程执行任务
- 线程总数达到 maximumPoolSize → 触发拒绝策略
顺序牢记:核心线程 → 队列 → 非核心线程 → 拒绝
三、四种拒绝策略(JDK 内置)
AbortPolicy(默认)
直接抛出
RejectedExecutionException,中断业务。CallerRunsPolicy
不抛异常、不丢弃;
交给提交任务的主线程自己执行,限速限流。
DiscardPolicy
默默丢弃当前新任务,无异常、无日志,容易丢数据。
DiscardOldestPolicy
丢弃队列最旧的任务,塞入当前新任务。
四、线程池 5 种状态 & 转换
5 种状态
- RUNNING:正常运行,接收新任务、处理队列任务
- SHUTDOWN:不接收新任务,继续执行队列剩余任务
- STOP:不接收新任务、中断正在执行的任务、清空队列
- TIDYING:所有任务结束,线程数为 0,准备收尾
- TERMINATED:线程池彻底关闭
状态流转
- RUNNING → shutdown() → SHUTDOWN
- RUNNING → shutdownNow() → STOP
- SHUTDOWN/STOP 任务跑完 → TIDYING → TERMINATED
底层存储
线程池用一个 AtomicInteger 打包:
高 3 位:线程池状态 | 低 29 位:当前线程数量
通过 CAS 原子修改,保证并发安全。
五、为什么禁止使用 Executors 快速创建线程池
阿里开发手册强制禁止,本质:资源不可控,极易引发线上故障
1. Executors 弊端
Executors.newFixedThreadPool()无界
LinkedBlockingQueue,任务无限堆积 → OOMExecutors.newSingleThreadExecutor()同样无界队列,积压打爆内存
Executors.newCachedThreadPool()最大线程数 Integer.MAX_VALUE → 极端情况无限创建线程,CPU 打满、OOM
无自定义拒绝策略,异常不可控
线程工厂默认,线程无意义名称,线上排查困难
2. 正确做法
手动 new ThreadPoolExecutor() 构造器:
手动指定有界队列
限制最大线程数
自定义拒绝策略(降级 / 告警 / 落地)
告警:
任务满了被拒绝 → 打日志、发监控、发短信 / 钉钉告警
作用:告诉运维 / 开发:系统压力炸了,有任务处理不过来。
降级:
任务满了被拒绝 → 放弃复杂逻辑,走简单兜底逻辑
落地(持久化):
任务实在处理不下、既不能降级、也不能丢 →
把任务数据 存数据库 / 存本地文件 / 丢到 MQ / 写入 Redis
自定义线程名,方便问题排查
六、补充关键细节
- 核心线程默认不超时回收,可通过
allowCoreThreadTimeOut开启回收 - 非核心线程靠
keepAliveTime自动销毁 - 队列一定要有界,是防止 OOM 最关键手段
- 生产环境拒绝策略不能用默认抛异常,要做业务降级、MQ 重试、日志落库
极简汇总
- 七大参数:核心线程、最大线程、空闲超时、阻塞队列、线程工厂、拒绝策略。
- 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝。
- 拒绝策略:抛异常、调用者执行、丢弃、丢弃最旧。
- 五种状态:RUNNING / SHUTDOWN / STOP / TIDYING / TERMINATED。
- 禁用 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 | |
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 | |
异常直接消失,控制台啥都没有!
submit() → 异常存在 Future 里,不打印
1 | |
你不调用 get() → 永远看不到异常!
二、3 种解决方法
方法 1:任务内部 强制 try-catch(最简单、最稳)
所有提交给线程池的任务,必须包一层 try-catch
1 | |
优点:
- 100% 捕获异常
- 日志必打
- 不会丢错误
方法 2:自定义 ThreadFactory,给线程设置UncaughtExceptionHandler
让线程崩溃前强制打印日志。
1 | |
优点:
- 全局统一处理
- 不用每个任务写 try-catch
方法 3:重写 ThreadPoolExecutor.afterExecute
线程池自带一个钩子方法:
任务执行完(包括异常)一定会调用!
1 | |
终极方案:
- 全局捕获
- 所有异常都跑不掉
- 生产标准用法