Java多线程线程池ThreadPoolExecutor理解总结:6 个核心参数 + 4 种拒绝策略(附完整示例)

ThreadPoolExecutor 讲透:6 个核心参数 + 4 种拒绝策略(附完整示例)

线程池的本质是两件事:限制并发上限 + 复用线程ThreadPoolExecutor 把这两件事"参数化"到了极致:线程什么时候创建、最多创建多少、任务往哪儿放、放不下怎么办、线程长什么样------全都能配。

  1. 提交任务时到底发生了什么(核心调度逻辑)
  2. 6 个关键参数逐个解释(怎么影响行为)
  3. 4 种拒绝策略逐个解释(队列满 + 线程也满时怎么办)

1. 先搞清楚:提交一个任务时,线程池按什么顺序"消化"它?

可以把 execute/submit 的处理流程记成一条很硬的规则链:

  1. 当前线程数 < corePoolSize

    → 直接创建新线程来跑这个任务(即使队列里还有空位也优先扩到核心线程数)

  2. 当前线程数 ≥ corePoolSize

    → 尝试把任务放进 workQueue

  3. 队列满了(放不进去)

    • 如果当前线程数 < maximumPoolSize
      → 继续创建新线程(非核心线程)来跑这个任务
    • 否则(线程也到上限了)
      → 触发拒绝策略 RejectedExecutionHandler

这一条链路把"什么时候扩线程、什么时候排队、什么时候拒绝"彻底定死了。


2. 6 个核心参数:每个参数到底控制什么?

2.1 corePoolSize:核心线程数("长期雇佣的正式员工")

  • 线程池希望长期维持的线程数量
  • 当任务来时,如果线程数不足核心数,倾向于立刻创建线程执行任务
  • 核心线程默认不回收 (除非显式开启 allowCoreThreadTimeOut(true)

直觉:corePoolSize 决定"常态吞吐的底盘"。


2.2 maximumPoolSize:最大线程数("极限扩容上限")

  • 队列满了以后,线程池还可以继续扩线程,但最多只能扩到 maximumPoolSize
  • 达到最大线程数后,再来任务且队列也满 → 只能拒绝

直觉:maximumPoolSize 决定"洪峰时最多能拉多少临时工"。

重要现实:如果 workQueue无界队列 (例如默认 LinkedBlockingQueue 不给容量),队列几乎不会满,线程池通常就很难扩到 maximum,表现会更像"固定线程池"。


2.3 keepAliveTime:非核心线程的空闲回收时间("临时工多久没活就解雇")

  • 主要作用于 超过核心数的那部分线程(非核心线程)
  • 当这些线程空闲超过 keepAliveTime,就会被回收,线程数逐步回落到 corePoolSize
  • 配合 TimeUnit 使用,比如 60 秒

直觉:keepAliveTime 决定"洪峰过去后回收速度"。

补充:开启 allowCoreThreadTimeOut(true) 后,核心线程也会按这个时间回收,适合极度节省资源的场景。


2.4 workQueue:任务队列("排队等候区")

任务先排队还是先扩线程,很大程度取决于队列类型。常见队列选择会直接改变线程池性格:

ArrayBlockingQueue(有界数组队列)
  • 有界,容量固定
  • 队列满了就会促使线程池扩线程,直到 maximumPoolSize
  • 更容易形成"排队有限 + 扩容明显 + 可控拒绝"的形态

适合:希望明确限制内存占用、希望洪峰时扩线程但别无限堆任务。

LinkedBlockingQueue(链表队列)
  • 可以有界,也可以无界(不传容量时近似无界)
  • 无界时:任务会疯狂堆积,线程数往往停在 corePoolSize 附近,maximumPoolSize 基本用不上
  • 风险:积压太多任务导致内存压力,甚至 OOM

适合:任务必须排队、洪峰也不想扩线程太猛、且能接受排队延迟的场景(通常建议给容量)。

SynchronousQueue(不存任务,直接"交接")
  • 容量为 0,没有真正的队列
  • 来一个任务就必须找到一个空闲线程接手;接不到就扩线程(直到 maximumPoolSize),再接不到就拒绝
  • 这会让线程池变得非常"激进扩容"

适合:任务很短、希望低延迟、不希望排队(更像 newCachedThreadPool 的核心组件)。


2.5 threadFactory:线程工厂("线程长什么样")

线程池创建新线程时会调用 threadFactory.newThread(runnable)。常用用途:

  • 给线程统一命名(排障神器)
  • 设置是否守护线程(daemon)
  • 设置优先级
  • 设置未捕获异常处理器(UncaughtExceptionHandler

默认线程名往往是 pool-1-thread-1 这种形式;生产环境强烈建议自定义命名,日志和 dump 会友好很多。


2.6 RejectedExecutionHandler:拒绝策略("队列满 + 线程到上限时的最后裁决")

当同时满足:

  • workQueue 放不下任务(满了)
  • 当前线程数已经达到 maximumPoolSize

此时线程池没法再接任务,只能交给拒绝策略处理。


3. 四种拒绝策略:到底"拒绝"是什么意思?

JDK 内置 4 个实现(名字就很直白):

3.1 AbortPolicy:直接抛异常(默认)

  • 直接抛 RejectedExecutionException
  • 最"硬",能让调用方立刻意识到系统过载

适合:任务不能悄悄丢、必须显式失败并被上层处理的场景。


3.2 CallerRunsPolicy:调用者执行(用提交线程"顶上去")

  • 不新建线程、不入队
  • 直接让提交任务的线程自己运行 task.run()

效果:提交方被"拖慢",形成一种天然的反压(backpressure),让系统自己降速。

适合:允许提交方变慢、希望通过反压保护系统的场景(例如网关层、消费端限流)。


3.3 DiscardOldestPolicy:丢最老(丢队列头部)再尝试提交

  • 丢弃队列中"等待最久"的那个任务(队首)
  • 然后再尝试把新任务放进去(通常会成功)

适合:更关心"最新任务",旧任务过期就没意义的场景(例如状态刷新、UI 更新、某些监控采样)。

风险:被丢的任务不会执行,必须确认业务允许。


3.4 DiscardPolicy:丢最新(直接丢当前任务)

  • 当前提交的任务直接被丢弃
  • 不抛异常

适合:允许少量任务悄悄丢失、且不希望影响主流程的场景(例如非关键日志、非关键统计上报)。

风险:最"静默",排障时容易误以为任务执行了。


4. 一份完整示例:把 6 个参数 + 拒绝策略串起来

来看这段最常用的配置模板:有界队列 + 明确最大线程 + 命名线程 + 自选拒绝策略

java 复制代码
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        int corePoolSize = 4;
        int maximumPoolSize = 8;
        long keepAliveTime = 60L;

        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);

        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger idx = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("biz-worker-" + idx.getAndIncrement());
                t.setDaemon(false);
                return t;
            }
        };

        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
        // 可替换:
        // new ThreadPoolExecutor.AbortPolicy()
        // new ThreadPoolExecutor.DiscardOldestPolicy()
        // new ThreadPoolExecutor.DiscardPolicy()

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                workQueue,
                threadFactory,
                handler
        );

        for (int i = 0; i < 1000; i++) {
            final int id = i;
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " running task " + id);
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
            });
        }

        pool.shutdown();
    }
}

理解这段配置时,抓住三件事就够了:

  • 常态并发 :由 corePoolSize 决定
  • 洪峰策略 :队列满后是否扩线程,由 workQueue + maximumPoolSize 决定
  • 过载策略 :扩也扩不动时怎么办,由 RejectedExecutionHandler 决定

5. 选型与调参的"工程化口诀"

  • 想要"可控" → 优先有界队列(别让任务无限堆)
  • 想要"低延迟不排队" → SynchronousQueue(但要小心线程暴涨)
  • 允许"降速保护系统" → CallerRunsPolicy 是最实用的反压手段
  • 任务绝不能丢 → 别用 Discard*,用 AbortPolicy 并在上层兜底
  • 排障要轻松 → 自定义 threadFactory 命名线程(dump 一眼看穿)

ThreadPoolExecutor 看成一个"可编程的调度器"会更顺手:
corePoolSize/maximumPoolSize/keepAliveTime 决定"人手规模与回收";workQueue 决定"排队策略";RejectedExecutionHandler 决定"过载时的业务选择"。

相关推荐
草履虫建模15 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
naruto_lnq17 小时前
分布式系统安全通信
开发语言·c++·算法
qq_2975746717 小时前
【实战教程】SpringBoot 实现多文件批量下载并打包为 ZIP 压缩包
java·spring boot·后端
老毛肚17 小时前
MyBatis插件原理及Spring集成
java·spring·mybatis
学嵌入式的小杨同学17 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
lang2015092817 小时前
JSR-340 :高性能Web开发新标准
java·前端·servlet
Re.不晚18 小时前
Java入门17——异常
java·开发语言
缘空如是18 小时前
基础工具包之JSON 工厂类
java·json·json切换
精彩极了吧18 小时前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合
追逐梦想的张小年18 小时前
JUC编程04
java·idea