摘要:线程池是 Java 后端异步并发最常用基础组件,很多线上故障根源都来自线程池参数配置错误、API 误用。本文从原理、核心参数、生产可用代码示例,结合3 起真实生产事故案例讲解,给出可落地的规范、监控与避坑方案,适用于订单、消息、异步通知等业务场景。
一、为什么要用线程池
直接new Thread()创建线程存在明显缺陷:
- 线程创建、销毁开销大,频繁创建会占用大量 CPU;
- 没有并发管控,高并发瞬间创建成千上万个线程,会触发
unable to create native thread,操作系统直接拒绝创建线程; - 无法统一监控、管理任务状态。
线程池核心价值:复用线程、控制并发、隔离资源、可观测。
开发规范(阿里 Java 开发手册强制要求):生产环境禁止使用 Executors 快速创建线程池,必须手动实例化
ThreadPoolExecutor。
Executors 封装的newFixedThreadPool、newSingleThreadExecutor底层是无界队列;newCachedThreadPool最大线程数为Integer.MAX_VALUE,大促 / 流量洪峰极易引发 OOM。
二、ThreadPoolExecutor 7 大核心参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)表格
| 参数 | 含义 | 生产关注点 |
|---|---|---|
| corePoolSize | 核心线程数 | 常驻线程,默认不回收,IO 密集型可适当调大 |
| maximumPoolSize | 最大线程数 | 核心 + 非核心线程上限,用来做流量熔断 |
| keepAliveTime | 非核心线程空闲超时 | 超过时间自动回收非核心线程,释放资源 |
| unit | 超时时间单位 | TimeUnit.SECONDS 等 |
| workQueue | 阻塞队列 | 必须使用有界队列!禁止无参 LinkedBlockingQueue |
| threadFactory | 线程工厂 | 自定义线程名称,日志、栈排查必备 |
| handler | 拒绝策略 | 队列满 + 线程数达上限,新任务处理策略 |
4 种内置拒绝策略
AbortPolicy:抛出RejectedExecutionException(默认,适合强一致性业务)CallerRunsPolicy:调用者线程执行任务(降级,不抛异常,注意:会阻塞 Tomcat 主线程)DiscardPolicy:静默丢弃任务,无日志,生产极少使用DiscardOldestPolicy:丢弃队列最老任务,尝试提交新任务
任务执行流程
- 任务提交,线程数 < corePoolSize → 创建核心线程执行任务
- 线程数 ≥ corePoolSize → 任务入阻塞队列排队
- 队列已满,线程数 < maximumPoolSize → 创建非核心线程执行任务
- 队列已满,线程数达到 maximumPoolSize → 执行拒绝策略
关键点:先入队,队列满了才扩容到最大线程,很多人误以为线程不够就直接扩容,这是常见误区。
三、生产可用代码示例
示例 1:基础业务线程池(推荐生产写法)
适用于异步通知、订单后置处理、数据库批量查询等 IO 密集场景。
import java.util.concurrent.*; /** * 生产级线程池示例:订单异步处理线程池 */ public class OrderAsyncThreadPoolDemo { public static void main(String[] args) { // 自定义线程工厂,命名,方便日志定位 ThreadFactory orderThreadFactory = new ThreadFactory() { private int seq = 1; @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "order-async-thread-" + seq++); // 设置非守护线程,业务任务执行完成才退出 t.setDaemon(false); return t; } }; // 生产线程池定义 ThreadPoolExecutor orderThreadPool = new ThreadPoolExecutor( 4, // 核心线程 8, // 最大线程 10L, // 非核心线程空闲10s回收 TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), // 有界队列,容量200,限制任务堆积上限 orderThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy() // 队列满,调用线程执行降级 ); // 模拟提交订单后置任务:短信通知、积分发放 for (int i = 1; i <= 300; i++) { int orderId = i; orderThreadPool.submit(() -> { try { System.out.printf("[%s] 处理订单后置任务,orderId=%d%n", Thread.currentThread().getName(), orderId); // 模拟IO耗时:调用短信RPC、数据库写积分 Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { // 任务内部捕获异常,防止线程意外退出 System.err.println("任务执行异常,orderId=" + orderId); } }); } // 应用关闭时优雅关闭线程池 shutdownThreadPool(orderThreadPool); } /** * 优雅关闭线程池工具方法 */ private static void shutdownThreadPool(ThreadPoolExecutor pool) { pool.shutdown(); try { // 等待5s,等待剩余任务执行 if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { // 超时强制中断 pool.shutdownNow(); } } catch (InterruptedException e) { pool.shutdownNow(); } } }示例 2:带返回值 Callable 任务,获取结果 + 超时控制
业务需要拿到异步任务返回结果,必须加 get 超时,防止无限阻塞
import java.util.concurrent.*; public class CallableThreadPoolDemo { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 4, 5L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); Future<Long> future = pool.submit(() -> { // 模拟统计计算 Thread.sleep(800); return 9999L; }); try { // 重点:设置超时,防止任务卡死导致主线程永久阻塞 Long result = future.get(3, TimeUnit.SECONDS); System.out.println("异步任务结果:" + result); } catch (TimeoutException e) { System.err.println("任务执行超时"); } catch (Exception e) { e.printStackTrace(); } pool.shutdown(); } }示例 3:定时任务线程池(ScheduledThreadPoolExecutor)
延迟执行、周期性巡检、定时重试场景
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ScheduledPoolDemo { public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(2); // 延迟2s执行一次 scheduledPool.schedule(() -> { System.out.println("延迟任务执行:订单状态巡检"); }, 2, TimeUnit.SECONDS); // 初始延迟1s,每3s执行一次(固定频率) scheduledPool.scheduleAtFixedRate(() -> { System.out.println("周期巡检:" + System.currentTimeMillis()); }, 1, 3, TimeUnit.SECONDS); } }四、生产事故复盘(真实业务场景)
事故 1:大促 OOM 宕机,根源:Executors.newFixedThreadPool 无界队列
现象:订单异步通知服务,大促期间内存持续上涨,频繁 Full GC,最终 OOM 宕机,大量用户收不到短信通知。
原始错误代码
// ❌ 线上错误写法,禁止使用 ExecutorService pool = Executors.newFixedThreadPool(10);根因:newFixedThreadPool底层LinkedBlockingQueue无界(容量Integer.MAX_VALUE)。短信下游通道变慢,10 个线程全部阻塞,新任务源源不断入队,任务对象持续堆积,占用堆内存,最终 OOM。
修复方案
- 替换为手动
ThreadPoolExecutor,使用ArrayBlockingQueue有界队列; - 设置队列最大容量,增加拒绝策略;
- 增加监控:队列长度、活跃线程、已完成任务数告警。
经验:IO 类异步任务,下游不稳定时,无界队列就是定时炸弹。
事故 2:拒绝策略选错,CallerRunsPolicy 阻塞 Tomcat 主线程
现象:接口响应超时,大量请求堆积,所有 http 接口变慢,数据库连接池耗尽。
背景:异步任务线程池队列满,使用CallerRunsPolicy。
根因:当队列满,线程池不再新建线程,任务交给调用者(Tomcat 工作线程)执行。大量耗时任务抢占 Tomcat 线程池,Tomcat 线程耗尽,新请求无法处理,接口雪崩。
修复方案
- 区分业务:核心链路不要直接用 CallerRunsPolicy;
- 重要业务:自定义拒绝策略,任务落库,后续后台重试;
- 非核心业务(日志上报):可以使用 CallerRunsPolicy,做好降级限流。
事故 3:任务未捕获异常,线程池线程消失,并发能力下降
现象:线程池配置核心线程 5,运行一段时间,活跃线程数降到 1~2,任务处理速度急剧下降,无 OOM。
根因:Runnable 任务抛出未捕获异常,线程直接终止,线程池会新建线程,但频繁异常会持续消耗资源。
submit 提交的任务,异常保存在 Future,只有调用 get () 才抛出;execute 提交任务,异常直接抛出,线程直接退出。
修复方案
- 所有任务内部
try-catch捕获异常,打印日志; - 自定义 ThreadFactory,设置
UncaughtExceptionHandler全局捕获线程异常; - 监控活跃线程数,出现异常下降触发告警。
五、生产环境最佳实践
1. 线程数配置参考公式
- CPU 密集型(大量计算):核心线程 ≈ CPU 核心数 + 1
- IO 密集型(RPC、DB、HTTP 调用):核心线程 ≈ CPU 核心数 * 2,可根据压测上浮
公式仅作为初始值,最终必须压测调优,不能硬套。
2. 业务隔离原则
不同业务不要共用同一个线程池。
例如:订单支付、消息推送、日志上报,分别创建独立线程池。
防止某一类任务阻塞,耗尽整个线程池资源,影响其他业务(资源隔离)。
3. 必须添加监控指标(接入 Prometheus / 监控平台)
- 核心指标:活跃线程数、队列当前大小、队列剩余容量、已完成任务数、拒绝任务数
- 告警规则:队列占用超过阈值、拒绝任务 > 0、活跃线程持续等于最大线程数,触发告警。
4. 异常处理规范
- Runnable 任务内部增加 try-catch,打印业务上下文(订单号、traceId);
- Future.get () 必须设置超时时间,避免无限阻塞;
- 线程池销毁:Spring 环境可交给 Spring 管理线程池,容器销毁自动 shutdown;独立应用需要手动优雅关闭。
六、常见踩坑清单
- ❌ 使用 Executors 工具类创建线程池,无界队列引发 OOM
- ❌ 队列不设上限,任务无限堆积
- ❌ 拒绝策略不评估业务,盲目使用 CallerRunsPolicy,阻塞 web 主线程
- ❌ 任务内不捕获异常,线程池线程丢失,并发能力下降
- ❌ Future.get () 无超时,任务卡死导致主线程阻塞
- ❌ 多个业务共用同一个线程池,一个业务拖垮整体
- ❌ 忘记关闭线程池,应用退出时线程不释放,资源泄漏
七、总结
线程池不是简单的 “开多线程” 工具,本质是并发资源的限流与隔离组件。
生产环境的核心原则:手动创建 ThreadPoolExecutor、有界队列、业务隔离、监控告警、合理拒绝策略、做好异常捕获。
绝大多数线程池故障,不是原理复杂,而是开发图省事直接使用 Executors,参数随意配置,缺少监控,等到流量洪峰才暴露问题。