Java · 线程池 ThreadPoolExecutor

七参数 · 反直觉的执行顺序 (core → 队列 → max → 拒绝) · 拒绝策略 · 为什么禁用 Executors 便捷工厂

ThreadPoolExecutor 七参数 (面试要求按顺序背出并解释) 1 corePoolSize 核心线程数 默认不回收 keepAlive 只裁 >core 的 2 maximumPoolSize 最大线程数 队列满后才扩到这里 (救急线程) 3 keepAlive + 4 unit 救急线程空闲存活时间 allowCoreThreadTimeOut 可让核心也参与回收 5 workQueue 任务队列 必须有界! ArrayBlockingQueue 等 6 threadFactory 线程命名 + daemon 排查问题的救命稻草 pool-3-thread-17 = 匿名 7 rejectedHandler 满载时的处置 默认 Abort 抛异常 生产常自定义降级 提交任务后的决策流 — 顺序反直觉! ① 线程数 < corePoolSize → 直接新建核心线程执行 ② core 满 → 任务先入队排队等待 (而不是扩线程!) ③ 队列也满 → 才扩到 maximumPoolSize (救急线程) ④ max 也满 → 执行 rejectedHandler 拒绝策略 后果: 无界队列 → maximumPoolSize 永远不生效 → 伪单线程 + 队列 OOM 四种拒绝策略 AbortPolicy — 抛 RejectedExecutionException (默认) CallerRunsPolicy — 提交线程自己跑 (天然反压) DiscardPolicy — 静默丢弃新任务 DiscardOldestPolicy — 丢队头最老任务 生产推荐: 自定义 — 记日志+计数+落 MQ/降级 CallerRuns 妙处: 提交方被拖慢 = 自动限流, 但会阻塞 Tomcat 线程, 网关类服务慎用 生命周期状态 (按位序只能左移) RUNNING SHUTDOWN 不收新·跑完剩余 STOP 不收新+中断 TIDYING 队列空·线程空 TERMINATED terminated() 完成 优雅停机: shutdown() 等存量 → awaitTermination(t) 超时 → shutdownNow() 中断 线程数怎么定 (起点, 不是终点) CPU 密集: N + 1 IO 密集: N × (1 + 等待/计算) Web 经验值 200-400 是压测出来的 公式只给起点, 容量必须压测定 Legend 七参数 决策流/正常路径 拒绝/警示 经验值

为什么要池化

  • • 线程创建/销毁是系统调用, 池化均摊成本
  • • 有界并发保护下游与自身内存
  • • 统一命名/监控/停机, 可运维

先入队, 后扩容

  • • 顺序: core → queue → max → reject
  • • 与直觉相反: 满载先排队不先加人
  • • 无界队列让 max 永远不生效

拒绝是设计点

  • • 默认 Abort 抛异常, 请求直接失败
  • • CallerRuns 反压: 提交方被拖慢
  • • 生产: 自定义"日志+计数+降级落地"

💡 一句话理解

线程池 = 参数化的"用工制度": 核心员工(core)常驻, 忙不过来先排队(队列), 队伍排满了才招救急工(max), 还不行就拒绝(Handler)。最反直觉也最常考的是顺序 — 先入队、后扩容; 最常出事的是队列 — 无界队列让 maximumPoolSize 形同虚设, 任务堆积到 OOM。这就是阿里规约"禁用 Executors、必须手动 new"的根本原因。

🧠 必知必会 必考 & 必会

Executors 四宗罪
newFixedThreadPool/newSingleThreadExecutor: 无界 LinkedBlockingQueue → 任务堆积 OOM; newCachedThreadPool: max=Integer.MAX → 线程爆炸; newScheduledThreadPool 同 cache 隐患。正解: 手动 new ThreadPoolExecutor 显式七参数。
Executors.newFixedThreadPool(10);   // 错: 无界队列 → 堆积 OOM
new ThreadPoolExecutor(10, 20, 60, SECONDS,
    new ArrayBlockingQueue<>(1000), ...);  // 对: 显式七参数
execute vs submit
execute 无返回值, 异常直接打到 UncaughtExceptionHandler; submit 返回 Future, 异常被包进 Future — 不调 get() 就静默吞掉, 任务"无声死掉"。生产必须二选一: get() 或 override afterExecute 记日志。
pool.execute(task);    // 异常直接打到 UncaughtExceptionHandler
Future<?> f = pool.submit(task);
f.get();               // 关键: 不 get() 异常被包进 Future 静默吞掉
keepAlive 与 core 回收
空闲回收只作用于 (core, max] 区间的救急线程; allowCoreThreadTimeOut(true) 可连核心一起收 — 低峰省资源, 适合流量波谷极深的长尾服务。
ThreadPoolExecutor p = new ThreadPoolExecutor(
    core, max, 60, SECONDS, queue);  // 只回收 (core, max]
p.allowCoreThreadTimeOut(true);  // 关键: 连核心一起收
线程数公式
CPU 密集 N+1(N=核数, 多 1 防缺页空转); IO 密集 N×(1+W/S)。公式只给起点: Web 服务 200~400 的经验值全部来自压测曲线 — 吞吐拐点与队列延迟一起看。
int n = Runtime.getRuntime().availableProcessors();
threads = n + 1;              // CPU 密集
threads = n * (1 + wait / service);  // IO 密集, 压测定终值
动态调参
setCorePoolSize/setMaximumPoolSize 运行时可改(美团动态线程池方案的基石): 压测/告警联动配置中心, 比重启扩容优雅得多。配套监控: 活跃数、队列水位、拒绝计数。
pool.setCorePoolSize(32);        // 运行时生效, 无需重启
pool.setMaximumPoolSize(64);
// 配套: 活跃数 / 队列水位 / 拒绝计数 → 配置中心联动
为什么队列必须有界
无界 = 反压失效: 下游变慢时任务无限堆积, 先打爆堆(任务对象+上下文), 而不是触发拒绝策略暴露问题。有界 + 合理拒绝 = 快速失败 + 可观测。
new LinkedBlockingQueue<Runnable>();    // 错: 无界, max 永不生效
new ArrayBlockingQueue<>(1000);       // 对: 有界+拒绝=快速失败
ThreadLocal 与线程池
线程被复用, ThreadLocal 不 remove = 下一任务读到脏数据 + Entry 泄漏。finally 里 remove 是铁律 (尤其 traceId/用户上下文)。
ctx.set(userId);
try { process(); }
finally { ctx.remove(); }  // 关键: 线程复用, 不 remove = 脏数据+泄漏

🏭 生产实战 real world

场景 1 · 标准的生产级线程池

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    8, 16,                                     // core / max: 压测起点
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500),                  // 有界! 水位=缓冲突发, 不是垃圾桶
    new ThreadFactoryBuilder()
        .setNameFormat("order-export-%d").build(),     // 命名: jstack 一眼定位
    new ThreadPoolExecutor.CallerRunsPolicy());       // 反压 (或自定义降级)

场景 2 · 监控水位 + 动态调参

// 暴露到 Prometheus:
gauge.set(pool.getActiveCount(), pool.getQueue().size(), rejectedCount.get())
// 告警联动配置中心 (Apollo/Nacos) 热调参, 不重启:
pool.setCorePoolSize(newCore);      // 运行时生效
pool.setMaximumPoolSize(newMax);
// 大促前: 队列水位 >80% → 自动 core 8→32, 平峰回收

场景 3 · submit 吞异常排查

Future<?> f = pool.submit(task);
try { f.get(3, TimeUnit.SECONDS); }          // 不 get: 任务死了都没声
catch (ExecutionException e) {
    log.error("task failed", e.getCause());   // 真实业务异常在 cause 里
}

场景 4 · Web 容器与业务池隔离

Tomcat 线程只做接入, 慢业务(导出/推送)必须独立池, 防互相拖垮:

// Tomcat: 快速接入 (server.tomcat.threads.max=200)
# 业务: 慢操作独立池 — 导出任务再慢也不影响下单接口
@Bean("exportPool")
public ThreadPoolExecutor exportPool() {
    return new ThreadPoolExecutor(4, 8, 60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(200),
        namedThreadFactory("export"),      // 命名区分: jstack 一眼归属
        new ThreadPoolExecutor.AbortPolicy());
}
# @Async("exportPool") 或手动 submit — 明确指定, 不用默认池

场景 5 · CompletionService: 谁先完成先处理

并行调 N 个下游, 按完成顺序消费而不是提交顺序等待:

ExecutorCompletionService<Quote> ecs =
        new ExecutorCompletionService<>(pool);
for (Vendor v : vendors) ecs.submit(() -> v.quote(req));

for (int i = 0; i < vendors.size(); i++) {
    Future<Quote> f = ecs.poll(500, MILLISECONDS);    // 谁先回先拿
    if (f != null) merge(f.get());               // 超时不候: 慢 vendor 不拖整体
}

场景 6 · CompletableFuture 编排 + 自定义池

声明式并发编排, 但默认池是 ForkJoinPool.commonPool — 生产必须显式传池:

CompletableFuture
    .supplyAsync(() -> queryUser(uid), ioPool)       // 显式业务池!
    .thenCombine(
        CompletableFuture.supplyAsync(() -> queryOrders(uid), ioPool),
        (user, orders) -> render(user, orders))
    .orTimeout(800, MILLISECONDS)                 // JDK9+ 整链超时
    .exceptionally(e -> fallbackPage());                 // 降级页兜底

场景 7 · 拒绝时落 MQ 延迟重试

自定义拒绝策略: 不丢任务, 转投消息队列削峰:

new ThreadPoolExecutor.RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        log.warn("pool saturated, queue={}", e.getQueue().size());
        rejectedCounter.inc();                          // 拒绝计数: 核心告警指标
        mqProducer.send("task-retry", serialize(r), delay(30s));  // 30s 后回放
    }
}

场景 8 · 舱壁隔离: 核心接口独立池

支付回调与营销推送共用池 = 推送风暴时支付被拖死; 按重要性分组隔离:

pools.put("payment", newPool(32, 32, 1000));   // 核心交易: 大配额
pools.put("marketing", newPool(4, 8, 200));    // 可降级: 小配额+丢弃策略
# 每个池独立监控水位与拒绝数 — 一个池打满, 告警只指向那个业务域

场景 9 · 压测找容量拐点

线程数不是猜的 — 固定负载扫描, 找吞吐拐点与队列水位的关系:

// 阶梯压测: 逐步加压, 每档记录三件事
for threads in 50 100 150 200 300; do
    run_load --threads=$threads --duration=120s
    record: QPS | P99 | pool.active + queue.size   # 三线同图
done
# 拐点特征: QPS 不再涨而 queue 持续涨 → 该档位就是容量上限, 参数定在拐点前 20%

场景 10 · ScheduledThreadPoolExecutor 的正确用法

定时任务池的两个必知: 异常会杀掉后续调度、任务要幂等:

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4,
        namedThreadFactory("sync"));
ses.scheduleWithFixedDelay(() -> {
    try { syncFromUpstream(); }               // 必须 try 包住!
    catch (Throwable t) { log.error("sync fail", t); }  // 抛出=该任务永久停摆
}, 0, 30, TimeUnit.SECONDS);                // fixedDelay: 上一轮跑完再计时

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 无界队列伪并发 — newFixedThreadPool 感觉"卡住不报错": max 永不生效, 任务堆到 OOM。正解: 有界队列 + 拒绝策略 + 队列水位告警。
Executors.newFixedThreadPool(8);  // 错: 无界队列, max 永不生效
new ThreadPoolExecutor(8, 16, 60, SECONDS,
    new ArrayBlockingQueue<>(500), handler);  // 对
坑 2 · Spring @Async 默认非池化 — 未配置时用 SimpleAsyncTaskExecutor: 每个任务新开线程, 高并发直接线程爆炸。正解: 实现 AsyncConfigurer 指定自定义 ThreadPoolTaskExecutor。
// 错: 不配置 → SimpleAsyncTaskExecutor, 每任务新开线程
// 对: 实现 AsyncConfigurer 返回自定义 ThreadPoolTaskExecutor
坑 3 · ThreadLocal 不 remove — 复用线程读到上个请求的用户信息 = 数据串号事故。正解: try/finally remove; 或用阿里 TransmittableThreadLocal(ttl) 做上下文传递。
userCtx.set(u);  // 错: 用完不 remove → 复用线程读到旧用户
try { process(); } finally { userCtx.remove(); }  // 对
坑 4 · submit 不 get 吞异常 — 失败任务无日志无告警。正解: 统一封装 submit 强制 get/afterExecute 记录; 或改用 CompletableFuture + exceptionally。
pool.submit(task);        // 错: 异常包进 Future 静默吞
pool.submit(task).get();  // 对: 或 afterExecute 统一记日志
坑 5 · 停机丢任务 — kill -9 或不等待: 队列任务直接蒸发。正解: shutdown hook 里 shutdown() → awaitTermination(30s) → shutdownNow(), 配合 MQ/落盘保证可重放。
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    pool.shutdown();                          // 1. 不收新任务
    pool.awaitTermination(30, SECONDS);        // 2. 等存量跑完(示意)
    pool.shutdownNow(); }));                   // 3. 超时强制收尾
坑 6 · 任务里再 submit 同一池等待子任务 — 线程都在等子任务、子任务在队列里 = 线程池死锁。正解: 父子任务分池; 或用 CompletableFuture 组合避免阻塞等待。
child.get();  // 错: 同池父子任务互等 → 池死锁
// 对: 父子分池, 或 CompletableFuture 组合不阻塞
坑 7 · 线程池当缓存用 — 提交百万任务"排队慢慢跑", 队列本身就是内存炸弹。正解: 大批量用分批 + 限流(信号量), 队列容量与消费速率匹配。
for (int i = 0; i < 1_000_000; i++)
    pool.submit(job);   // 错: 百万任务排队 = 内存炸弹
// 对: 分批 + Semaphore 限流, 队列容量匹配消费速率
坑 8 · core=max 没有救急线程 — 突发流量全部只能排队, max 形同虚设。正解: core 定常态负载, max 留 2 倍余量吸收突发, 配合 keepAlive 自动回收。
(core = 16, max = 16)               // 错: 突发只能排队
(core = 16, max = 32, keepAlive = 60s)  // 对: 救急余量
坑 9 · core 设过高引发切换风暴 — 64 核机器给 IO 型服务 core=512: 上下文切换吃掉 CPU。正解: 线程数看下游容量与等待比, 不看机器核数; 切换次数(vmstat cs)纳入监控。
core = 512;  // 错: IO 型不看下游容量盲目拉高 → 切换风暴
vmstat 1     # 对: 看 cs 列; 线程数由下游容量与等待比决定
坑 10 · keepAlive=0 的抖动 — 救急线程建完即销毁, 反复建线程。正解: keepAlive 给 30-60s, 让突发期线程活过波峰。
keepAlive = 0s;   // 错: 救急线程建完即毁, 反复建线程
keepAlive = 60s;  // 对: 让突发期线程活过波峰
坑 11 · 队列太小频繁触发拒绝 — queue=10 时稍慢就拒绝, 之前设的 max 救急线程永远在"救火边缘"。正解: 队列容量 = 突发持续时长 × 消费速率, 配水位告警观察。
queue = 10;  // 错: 稍慢就拒绝, max 永在救火边缘
// 对: 容量 = 突发时长 × 消费速率, 配水位告警
坑 12 · CallerRuns 阻塞 Tomcat 线程 — 反压落到 HTTP 线程上, 请求线程被业务跑满, 接入层整层卡住。正解: 网关/在线服务用"落 MQ/降级"策略; CallerRuns 只适合离线任务。
new ThreadPoolExecutor.CallerRunsPolicy();  // 错: 反压压垮 HTTP 线程
// 对: 在线服务落 MQ / 降级; CallerRuns 只用于离线
坑 13 · Discard 系无感知丢任务 — 静默丢弃连日志都没有, 丢单都不知道。正解: 生产禁用裸 Discard; 一律自定义(记日志+计数+补偿)。
new ThreadPoolExecutor.DiscardPolicy();  // 错: 静默丢单
(r, e) -> { log.error("rejected", r); metric.inc(); }  // 对
坑 14 · execute 与 submit 混用无章法 — 异常处理语义完全不同, 混用导致"有的任务炸了知道, 有的静默死"。正解: 团队统一一种风格 + 统一的异常出口(afterExecute 或强制 get)。
// 错: 一处 execute 一处 submit, 异常出口不一致
// 对: 团队统一一种 + afterExecute / 强制 get 兜底
坑 15 · 任务抛 Error 级异常 — OOM/StackOverflow 也会被 submit 的 Future 吞掉, 掩盖致命问题。正解: afterExecute 里区分 Throwable 输出; Error 类任务直接停机告警。
// 错: OOM 也被 submit 的 Future 吞掉, 掩盖致命问题
protected void afterExecute(Runnable r, Throwable t) { ... }  // 对
坑 16 · 动态调参 max < core — setMaximumPoolSize 设成小于当前 core 直接 IllegalArgumentException。正解: 调参顺序: 先调 core 再调 max, 且保证 max ≥ core, 封装成原子操作。
pool.setMaximumPoolSize(4);  // 错: 小于当前 core → IllegalArgumentException
// 对: 先 setCorePoolSize 再 setMaximumPoolSize, max ≥ core
坑 17 · 定时任务抛异常后停摆 — scheduleAtFixedRate 的任务一旦抛出未捕获异常, 后续全部不再执行(静默)。正解: 任务体整体 try-catch; 监控"上次成功执行时间"做活性检测。
sched.scheduleAtFixedRate(() -> {
    try { job(); } catch (Throwable e) { log.error(e); }  // 对
}, 0, 1, MINUTES);  // 错: 裸抛一次 → 后续全部静默停摆
坑 18 · 停机期间提交未处理 RejectedExecutionException — shutdown 后 submit 抛拒绝异常, 调用方没接就是 500。正解: 优雅停机阶段入口层先切流量(注册中心摘除), 代码里 catch 拒绝异常转提示。
pool.submit(task);  // 错: shutdown 后 → RejectedExecutionException → 500
// 对: 停机先摘流量, catch 拒绝异常转友好提示
坑 19 · ThreadFactory 用默认 daemon 配置 — daemon 线程随 JVM 直接退出, 排队任务全丢; 反之非 daemon 阻止退出挂起停机。正解: 按语义定 daemon(后台任务 daemon + 显式停机等待; 关键任务非 daemon)。
// 错: 随手 daemon=true → JVM 退出排队任务全丢
// 对: 后台任务 daemon+显式停机等待; 关键任务非 daemon
坑 20 · 忽视 ForkJoinPool.commonPool 的坑 — CompletableFuture 默认用它: 容器限核时 parallelism 可能=1, 任务全串行。正解: 所有 supplyAsync/parallelStream 显式指定池或评估过 commonPool 容量。
CompletableFuture.supplyAsync(job);      // 错: 默认 commonPool, 容器限核或=1 全串行
CompletableFuture.supplyAsync(job, ioPool);  // 对: 显式指定池