线程池 ThreadPoolExecutor:Java并发的智能生产线调度系统
ThreadPoolExecutor 就是 Java 并发界的金牌包工头,手里攥着一批现成的工人(核心线程),门口还蹲着一群临时工(非核心线程),来活了不用现招工(新建线程),直接派活,活少了还能裁人,把线程的创建、销毁、调度玩出花来 —— 彻底解决 “每次来活都现拉人,干完活就散伙” 的资源浪费问题,主打一个高效、省钱、不内卷,老板喜欢。
这包工头够狠,因为它解决了线程的两大致命问题 ——
- 线程创建 / 销毁的高昂开销(系统创建线程要分配栈空间、寄存器等资源,销毁也要回收,反复操作极耗性能)
- 线程数量失控的并发灾难(线程过多会导致 CPU 上下文切换频繁,内存占用飙升,系统直接卡死)
主打一个资源复用、高效调度、稳控并发,让多线程任务执行告别混乱,全程井井有条。就像工地不能来 1 个小活招 100 个工人,也不能永远养着 100 个工人干闲活,ThreadPoolExecutor 就是那个能精准控人数、巧调度的包工头。
一、调度管家的核心工作规则(7 大核心参数)
ThreadPoolExecutor 的核心就是 7 个参数,相当于包工头的招工规则、场地大小、临时工制度、辞退规则,少一个都玩不转。
// 核心构造方法,7个参数一个不能少 public ThreadPoolExecutor( int corePoolSize, // 核心线程数(正式工数量) int maximumPoolSize, // 最大线程数(工地最多能容下的工人总数:正式工+临时工) long keepAliveTime, // 空闲时间(临时工没活干的超时时间,到点就裁,狠吧) TimeUnit unit, // 时间单位(秒/毫秒,配合空闲时间用) BlockingQueue<Runnable> workQueue, // 任务队列(待搬的砖堆,正式工忙不过来就堆这) ThreadFactory threadFactory, // 线程工厂(招工的人,给工人起名字、定规矩) RejectedExecutionHandler handler // 拒绝策略(砖太多堆不下、工人也满了,怎么处理新砖) )当然这老板有多少个固定员工,多少个外包,这根据自己的业务都有定数,你不能一心想成为那个🖤包工头,并为此奋斗不息,把一个线程当3+个线程用,还经常一言堂、动不动就搞压榨机,机器都干冒烟了这是不是很过分!
1. corePoolSize:正式工,铁饭碗不辞退
核心固定线程资源,核心正式工,只要线程池处于运行状态,哪怕没任务执行,也会一直保留(除非手动开启allowCoreThreadTimeOut)
- 来活了,新任务到来,先看正式工有没有闲着的,有就直接派活;
- 正式工全忙了,才会把活堆到任务队列,而不是立马招临时工。比如:corePoolSize=5,就是固定养 5 个正式工,常年在岗,随叫随到。
2. maximumPoolSize:线程池最大规模,工地最大容纳人数
能调度的线程资源天花板,包工头的招工上限,一旦任务队列堆满了,正式工全在忙,才会开始招临时工,直到工人总数达到这个数
- 该数值必须≥corePoolSize,不然临时工没名额,否则临时线程没有创建空间;
- 临时线程只是 “应急人手”,没活干到点就被裁;任务量下降后会被及时回收,不会长期占用资源。比如:corePoolSize=5,maximumPoolSize=10,就是管家最多可创建 5 个临时线程,线程池内总线程数绝不超过 10。
3. keepAliveTime + unit:临时线程的空闲回收时限
临时工的下岗倒计时😂,只要临时工没活干的时间达到这个值,直接裁掉,节省工地成本,对临时线程的资源回收规则,如果临时线程空闲无任务的时间达到这个值,直接回收该线程,释放系统资源,做到 “人走茶凉,不浪费资源”,这事放到人身上、充满了无奈,努力成为核心,现在这个大环境,自己才能保住自己,再说有本事在哪也不怕,共勉吧大家
- 若调用
executor.allowCoreThreadTimeOut(true),正式工没活干也会被裁,相当于包工头连正式工都敢开,主打一个极致省钱、不要脸。比如:keepAliveTime=60,unit=TimeUnit.SECONDS,就是临时工闲 1 分钟就卷铺盖走人。
4. workQueue:任务队列,待搬的砖堆
正式工全忙了之后,新任务不会立马招临时工,而是先堆到任务队列里,等正式工干完活再从队列里取活干。这是线程池的核心缓冲机制,避免一点活就招临时工,频繁招工裁人。常用的队列就 3 种,各有脾气:
- ArrayBlockingQueue:有界数组队列(固定大小的砖堆),满了就不能堆了,适合控制任务量,避免内存溢出;
- LinkedBlockingQueue:无界链表队列(无限大的砖堆),能一直堆活,缺点是活太多会把内存撑爆,此时 maximumPoolSize 相当于失效(永远招不到临时工);
- SynchronousQueue:同步队列(没地方堆砖),急性子、等不来一点,来活了必须立马有工人干,没工人就招临时工,直到达到最大数,适合任务需要快速处理的场景。
5. threadFactory:线程工厂,招工的 HR
负责创建新线程,相当于工地的 HR,给工人(线程)起名字、设置优先级、是否为守护线程等。
- JDK 默认提供
Executors.defaultThreadFactory(),但实际开发建议自定义 —— 比如给线程起名字pool-1-thread-1,方便排查问题(总不能出问题了,连哪个工人干的活都不知道)
import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class CustomThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger threadNumber = new AtomicInteger(1); public CustomThreadFactory(String poolName) { this.namePrefix = poolName + "-thread-"; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); // 关键:设置可读性强的线程名,方便出问题时 grep 日志 t.setName(namePrefix + threadNumber.getAndIncrement()); // 设置为非守护线程,确保任务能执行完 t.setDaemon(false); // 设置优先级 (可选) t.setPriority(Thread.NORM_PRIORITY); return t; } }6. handler:拒绝策略,砖太多了装不下,咋处理?
当任务队列堆满 + 工人总数达到最大值,新任务再来,包工头就没辙了,只能执行拒绝策略,这是线程池的最后一道防线,避免任务无限积压搞崩系统。
JDK 默认提供 4 种拒绝策略,全是狠角色,按需选择:
- AbortPolicy(默认):直接抛异常
RejectedExecutionException,主打一个 “老子不干了,报错给你看”; - CallerRunsPolicy:让提交任务的线程自己干,比如主线程提交任务被拒绝,主线程就自己执行,主打一个 “谁派活谁干,别难为我”;
- DiscardPolicy:直接丢弃新任务,悄无声息,主打一个 “眼不见心不烦,丢了也不告诉你”;
- DiscardOldestPolicy:丢弃任务队列里最老的任务,把位置让给新任务,主打一个 “喜新厌旧,留新丢旧”。
import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import lombok.extern.slf4j.Slf4j; @Slf4j public class CustomRejectedHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 1. 记录详细日志 (核心数、最大数、队列大小) log.error("线程池已满!任务被拒绝。核心数:{}, 最大数:{}, 队列大小:{}, 活跃线程:{}", executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getQueue().size(), executor.getActiveCount()); // 2. 业务降级策略 (三选一) // 策略 A: 丢弃并通知 (静默失败,适用于非核心业务) // return; // 策略 B: 让调用者自己跑 (适用于实时性要求高,不能丢失的任务) // r.run(); // 策略 C: 抛出自定义业务异常 (适用于核心业务,需前端感知) throw new RuntimeException("系统繁忙,请稍后重试 [CustomReject]"); // *生产环境最佳实践*: 这里通常会写入 MQ 或 Redis,后续异步补偿 // mqProducer.sendRetryTask(r); } }实际开发中,默认的 AbortPolicy 慎用(容易抛异常崩业务),一般自定义拒绝策略 —— 咱一般把任务丢到消息队列(MQ)里,后续慢慢处理,主打一个 “留后路”
来一个标准线程池吧
import java.util.concurrent.*; public class ProductionThreadPool { // 单例线程池实例 private static final ThreadPoolExecutor EXECUTOR; static { int cpuCores = Runtime.getRuntime().availableProcessors(); EXECUTOR = new ThreadPoolExecutor( // 1. 核心线程数 (IO 密集型) cpuCores * 2, // 2. 最大线程数 cpuCores * 4, // 3. 空闲存活时间 60L, TimeUnit.SECONDS, // 4. 有界队列 (防止 OOM) new ArrayBlockingQueue<>(1000), // 5. 自定义线程工厂 (好名字) new CustomThreadFactory("BizOrderPool"), // 6. 自定义拒绝策略 (留后路) new CustomRejectedHandler() ); // 7. 允许核心线程超时回收 (极致省钱模式,可选) // EXECUTOR.allowCoreThreadTimeOut(true); } /** * 提交任务 */ public static void execute(Runnable task) { EXECUTOR.execute(task); } /** * 提交带返回值任务 */ public static <T> Future<T> submit(Callable<T> task) { return EXECUTOR.submit(task); } /** * 【关键】打印监控指标 (对接 Prometheus 或定时日志) */ public static void printMetrics() { System.out.println("=== 线程池监控指标 ==="); System.out.println("核心线程数: " + EXECUTOR.getCorePoolSize()); System.out.println("当前线程数: " + EXECUTOR.getPoolSize()); System.out.println("最大线程数: " + EXECUTOR.getMaximumPoolSize()); System.out.println("活跃线程数: " + EXECUTOR.getActiveCount()); System.out.println("队列等待任务: " + EXECUTOR.getQueue().size()); System.out.println("队列剩余容量: " + (1000 - EXECUTOR.getQueue().size())); System.out.println("已完成任务: " + EXECUTOR.getCompletedTaskCount()); System.out.println("总任务数: " + EXECUTOR.getTaskCount()); System.out.println("====================="); } /** * 优雅关闭 */ public static void shutdown() { EXECUTOR.shutdown(); try { if (!EXECUTOR.awaitTermination(60, TimeUnit.SECONDS)) { EXECUTOR.shutdownNow(); } } catch (InterruptedException e) { EXECUTOR.shutdownNow(); Thread.currentThread().interrupt(); } } // 测试入口 public static void main(String[] args) throws InterruptedException { // 提交 10 个任务 for (int i = 0; i < 10; i++) { execute(() -> { try { Thread.sleep(2000); } catch (Exception e) {} System.out.println("执行任务 by: " + Thread.currentThread().getName()); }); } // 等待一秒看监控 Thread.sleep(1000); printMetrics(); shutdown(); } }二、包工头的派活流程:一步都不能乱
包工头派活的逻辑特别简单,就5 步,按顺序来,绝不乱搞:说完大家都去当包工头哦
来活(提交任务)→ 看正式工是否空闲 → 闲则派活,忙则堆队列 → 队列满了则招临时工 → 临时工招满则拒绝
- 线程池刚创建,正式工还没启动(懒加载,第一次来活才创建正式工);
- 提交任务,判断核心线程数是否达到 corePoolSize:没达到,创建新的正式工执行任务;达到了,走下一步;
- 判断任务队列是否满:没满,把任务加入队列;满了,走下一步;
- 判断工人总数是否达到 maximumPoolSize:没达到,创建临时工执行任务;达到了,走下一步;
- 执行拒绝策略,处理新任务。
划重点:先核心线程,再任务队列,最后临时工,这个顺序是线程池的灵魂,别搞反了!
注意:corePoolSize=5,workQueue 是无界队列,那永远不会招临时工,因为队列能一直堆活,maximumPoolSize 就成了摆设。
三、线程池的核心状态:包工头的工地运营模式
ThreadPoolExecutor 内部用一个32 位的 int 变量 ctl做状态 + 线程数的双重管理
- 和 ReentrantReadWriteLock 的 state 位拆分异曲同工
- 高 3 位表示线程池状态,低 29 位表示当前工作线程数。
- RUNNING(运行中):包工头正常营业,能接活能派活,默认状态;
- SHUTDOWN(关闭中):包工头不接新活了,但会把队列里的活干完,工人干完活再下班;
- STOP(强制停止):包工头不接新活,也不处理队列里的活,直接让所有工人停手下班,打断正在执行的任务;
- TIDYING(整理中):所有工人都下班了,任务也全处理完了,线程池进入整理状态,准备收尾;
- TERMINATED(已终止):整理工作完成,线程池彻底凉凉,无法再使用。
状态之间是单向切换的,不能回滚,比如 RUNNING 能切到 SHUTDOWN/STOP,但 STOP 不能切回 RUNNING,就像工地关了就不能再开,除非重新建一个。
触发状态切换的核心方法:
shutdown():温柔关闭,对应 SHUTDOWN 状态,不杀工人,干完活再走;shutdownNow():暴力关闭,对应 STOP 状态,直接杀工人,中断任务;awaitTermination():等待线程池进入 TERMINATED 状态,相当于等工地彻底清场。
四、为啥不用 Executors 创建线程池?包工头的坑要避开
DK 给了一个工具类Executors,能快速创建线程池,比如Executors.newFixedThreadPool()、Executors.newCachedThreadPool(),但阿里开发手册明令禁止使用,原因就一个:底层参数写死,容易搞崩系统,相当于包工头的规则被硬编码,遇到特殊情况直接翻车。
- newFixedThreadPool(n):核心线程数 = 最大线程数 = n,使用无界队列 LinkedBlockingQueue;→ 坑:任务无限积压,直接撑爆内存(OOM);
- newCachedThreadPool():核心线程数 = 0,最大线程数 = Integer.MAX_VALUE(约 21 亿),使用 SynchronousQueue;→ 坑:来活就招临时工,工人数量直接失控,CPU 被占满;
- newSingleThreadExecutor():单线程,无界队列;→ 坑:和 FixedThreadPool 一样,任务积压导致 OOM。
结论:手动创建 ThreadPoolExecutor,自定义 7 个核心参数,根据业务场景调整核心线程数、队列大小、拒绝策略,这才是并发开发的正确姿势。
五、实战技巧:包工头的最优管理方案
光懂原理不够,实际开发中把线程池用好,才是真本事,分享几个金牌包工头的运营技巧,全是干货:
1. 核心线程数怎么配?别拍脑袋,看业务
核心线程数是线程池的核心,配少了任务积压,配多了线程竞争,一般分两种场景:
- CPU 密集型任务(比如计算、排序):核心线程数 = CPU 核心数 + 1,因为 CPU 核心数是硬件上限,多了线程会抢 CPU,上下文切换开销大,+1 是为了防止某个线程阻塞导致 CPU 空闲;
- IO 密集型任务(比如数据库查询、网络请求):核心线程数 = CPU 核心数 ×2,因为 IO 操作时线程会空闲,多配点线程能充分利用 CPU,也可以按CPU 核心数 /(1 - 阻塞系数)计算(阻塞系数一般 0.8~0.9)。
实用小技巧:用Runtime.getRuntime().availableProcessors()获取当前机器的 CPU 核心数,动态配置,避免硬编码。
2. 任务队列用有界的,别用无界的
无论如何,都要使用ArrayBlockingQueue这类有界队列,设置合理的队列大小(比如 1000),原因:
- 无界队列会导致任务无限积压,最终 OOM;
- 有界队列能配合最大线程数,触发临时工招聘和拒绝策略,形成 “缓冲 - 限流 - 拒绝” 的完整机制。
3. 自定义线程工厂,给线程起名字
默认的线程名字是pool-1-thread-1,如果项目中有多个线程池,排查问题时根本分不清是哪个线程池的线程出了问题。
ThreadFactory customFactory = new ThreadFactory() { private final AtomicInteger count = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("biz-pool-thread-" + count.getAndIncrement()); // 业务相关的名字 thread.setDaemon(false); // 非守护线程,避免被JVM随意杀死 return thread; } };4. 自定义拒绝策略,别直接抛异常
默认的 AbortPolicy 直接抛异常,会导致业务报错,实际开发中建议自定义拒绝策略,比如:
- 把任务写入 MQ/Redis,后续消费重试;
- 记录日志 + 告警,通知开发人员处理;
- 降级返回,给用户友好提示。
RejectedExecutionHandler customHandler = (r, executor) -> { log.error("线程池已满,核心数:{},最大数:{},队列大小:{},任务被拒绝", executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getQueue().size()); throw new CustomBizException("系统繁忙,请稍后再试"); };5. 监控线程池,别当甩手掌柜
线程池用起来后,必须加监控,不然出问题了都不知道,核心监控指标:
- 核心线程数、当前工作线程数、最大线程数;
- 任务队列的已排队数、剩余容量;
- 已完成任务数、被拒绝的任务数;
- 线程池的运行状态。
JDK 提供了线程池的获取方法,直接用就行:getCorePoolSize()、getActiveCount()、getQueue().size()、getRejectedExecutionCount()等,把这些指标暴露到 Prometheus/Grafana,或打印到日志,实时监控。
6. 线程池用完要关闭,别内存泄漏
线程池的核心线程是常驻的
- 若线程池是局部变量,不用了不关闭,会导致核心线程一直占用资源,内存泄漏;
- 若线程池是全局单例(比如项目启动时创建,一直用到项目关闭),则不用手动关闭,项目停止时会自动销毁。
executor.shutdown(); // 温柔关闭,不接新活,干完队列里的活 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 60秒还没干完,暴力关闭 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { log.error("线程池关闭失败"); } }六、最后总结:ThreadPoolExecutor 的核心价值
ThreadPoolExecutor 本质是线程的池化技术,和数据库连接池、连接池的思想一致,都是为了解决资源的创建 / 销毁开销大、资源数量失控的问题;
用核心线程保基础效率,用任务队列做缓冲,用临时工应对峰值,用拒绝策略做限流,用监控做兜底,最终实现线程资源的高效利用,让并发业务既不卡壳,也不崩掉。
而它和 synchronized、AQS、CAS 的关系,就是 Java 并发的 “全家桶”:synchronized 是基础锁,CAS 是无锁并发的核心,AQS 是锁和线程池的底层框架,ThreadPoolExecutor 是 AQS 的上层高级应用 —— 懂了底层,再看线程池,就像看包工头搬砖,一眼看透本质。
最后补一句:线程池不是银弹,合理配置才是王道,别指望一个线程池走天下,根据业务拆分成多个线程池(比如核心业务池、非核心业务池),才是高级架构师的操作~
