线程池隔离策略 / 拒绝策略怎么选

一句话速记

线程池隔离 = 不同业务用不同线程池,防止慢接口耗尽线程拖垮快接口(Bulkhead 舱壁模式)。拒绝策略 4 种AbortPolicy(抛异常,默认)/ CallerRunsPolicy(调用者线程执行,有反压效果)/ DiscardPolicy(静默丢弃)/ DiscardOldestPolicy(丢最旧任务)。实际选型:重要任务用 CallerRunsPolicy + 告警;非重要任务(如日志)用 Discard;任何方案都要加监控。

通俗解释(5 分钟版)

为什么要隔离

❌ 共用一个线程池(风险):
   业务 A(快接口 10ms)
   业务 B(慢接口 5s,调 DB)
   
   B 突然 DB 慢,B 的任务积压 → 占满线程池 →
   A 的任务也排不进去 → A 也超时 →
   原本健康的接口被拖垮
   
✅ 隔离线程池(舱壁模式):
   pool-A(快接口)= 20 线程
   pool-B(慢接口)= 10 线程
   
   B 的 pool 满了 → 只影响 B,A 正常服务

线程池参数回顾

new ThreadPoolExecutor(
    corePoolSize,     // 核心线程数(常驻)
    maximumPoolSize,  // 最大线程数
    keepAliveTime,    // 非核心线程空闲多久回收
    unit,
    workQueue,        // 任务队列(SynchronousQueue / LinkedBlockingQueue 等)
    threadFactory,    // 线程命名
    rejectedHandler   // 拒绝策略
)

关键细节

1)线程池拒绝时机

任务来了 → 核心线程数够 → 直接用核心线程
核心线程满了 → 加入队列
队列满了 → 创建非核心线程(直到 maxPoolSize)
最大线程数满且队列也满 → 触发拒绝策略

注意顺序:先填满核心线程 → 再填满队列 → 再扩到 max 线程 → 再拒绝。这意味着:

  • LinkedBlockingQueue(无界) → 永远不触发拒绝策略(但内存溢出风险)
  • SynchronousQueue → 没有缓冲,直接扩线程,适合立即执行场景
  • LinkedBlockingQueue(有界) → 推荐:有缓冲 + 有上限

2)四种拒绝策略

// 1. AbortPolicy(默认):抛 RejectedExecutionException
// 调用方必须捕获并处理
// 适用:明确需要感知失败的场景(接口层面返回错误)
new ThreadPoolExecutor.AbortPolicy()
 
// 2. CallerRunsPolicy:由提交任务的线程(通常是主线程/web线程)执行
// 效果:天然反压——提交线程被占用,无法继续提交,减慢速度
// 适用:吞吐优先、能接受延迟、不能丢弃的任务
new ThreadPoolExecutor.CallerRunsPolicy()
 
// 3. DiscardPolicy:静默丢弃新任务
// 无任何通知!适用:日志、埋点等非关键任务
new ThreadPoolExecutor.DiscardPolicy()
 
// 4. DiscardOldestPolicy:丢弃队列中最旧的任务,然后重试提交
// 适用:需要保留"最新"数据的场景(如实时监控刷新)
// 风险:可能丢弃已经等了很久的任务
new ThreadPoolExecutor.DiscardOldestPolicy()

3)自定义拒绝策略(生产推荐)

// 实战最推荐:拒绝时告警 + 降级处理
public class AlertingAbortPolicy implements RejectedExecutionHandler {
    private final String poolName;
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1. 告警(必须!)
        log.error("[ThreadPool] {} rejected! queue={}, active={}, max={}",
            poolName,
            executor.getQueue().size(),
            executor.getActiveCount(),
            executor.getMaximumPoolSize()
        );
        Metrics.counter("thread_pool_rejected", "pool", poolName).increment();
        
        // 2. 降级处理(根据业务选择)
        throw new RejectedExecutionException("ThreadPool " + poolName + " overloaded");
    }
}

4)线程池隔离的粒度

按业务类型隔离

┌────────────────────────────────────────────────────────┐
│ 核心业务(订单/支付)     pool-critical  20核+200队列   │
│ 异步通知(短信/邮件)     pool-notify    10核+500队列   │
│ 报表/数据导出            pool-report    5核+50队列      │
│ 外部 HTTP 调用           pool-http      50核+100队列    │
│ 内部 DB 查询            pool-db        30核+200队列    │
└────────────────────────────────────────────────────────┘

Hystrix/Resilience4j 的线程池隔离(Circuit Breaker):

// Resilience4j 线程池隔离示例
ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.of("db-pool",
    ThreadPoolBulkheadConfig.custom()
        .maxThreadPoolSize(10)
        .coreThreadPoolSize(5)
        .queueCapacity(100)
        .build()
);
 
// 每个下游服务单独一个 bulkhead,互不影响

5)线程池参数调优经验

CPU 密集型

corePoolSize = CPU核数 (或 CPU核数+1)
maxPoolSize  = CPU核数 * 2
queue        = 小(SynchronousQueue 或小有界队列)

I/O 密集型

corePoolSize = CPU核数 * 2~4(甚至更多,取决于 I/O 比例)
maxPoolSize  = corePoolSize * 2
queue        = 有界(防止无限积压)

经验公式:
  线程数 = CPU核数 × (1 + I/O等待时间 / CPU计算时间)
  如果 I/O 占 90%,CPU 占 10%,8核机器 → 8 * (1 + 9) = 80 线程

监控指标

# 线程池健康监控(Spring Actuator + Micrometer)
executor.active        # 当前活跃线程数
executor.queued        # 队列中等待任务数
executor.completed     # 已完成任务数
executor.pool.size     # 当前池大小
thread_pool_rejected   # 拒绝次数(告警关键指标)
 
# 告警阈值参考:
# queued > maxPoolSize * 2 → 预警
# rejected > 0 → 立即告警

6)动态线程池(生产最佳实践)

// 固定线程数不好调,推荐动态配置
// 美团 DynamicTp、自研 + Apollo/Nacos 配置中心
ThreadPoolExecutor pool = new ThreadPoolExecutor(...);
 
// 运行时调整,不重启
pool.setCorePoolSize(newCore);
pool.setMaximumPoolSize(newMax);
// 注意:setMaximumPoolSize 必须 >= setCorePoolSize
 
// 配合告警:rejected > 0 → 人工介入调参或扩容

延伸追问

  • Q:CallerRunsPolicy 有什么风险? → 主线程(如 Tomcat 请求线程)被占用来执行业务任务,导致 HTTP 请求无法接收——相当于整个服务器被”降速”。适合吞吐敏感、能接受响应变慢的批量处理场景;不适合对延迟有 SLA 要求的接口
  • Q:为什么线程池要命名线程(ThreadFactory)? → 排查时 jstack 看到 pool-1-thread-1 完全不知道是哪个业务,order-db-pool-1 一目了然。必须命名
  • Q:Executors.newFixedThreadPool(n) 有什么坑? → 内部用的是无界 LinkedBlockingQueue,队列永不满 → 拒绝策略永不触发 → 任务无限积压 → 内存 OOM。生产不推荐 Executors 工厂方法,用 ThreadPoolExecutor 直接构造并指定有界队列。

我的记法

  • 隔离 = 舱壁模式:慢接口不拖垮快接口
  • 拒绝策略选型:重要任务 CallerRunsPolicy(反压)/ 非关键任务 Discard / 默认用自定义告警策略
  • 拒绝触发条件:核心线程满 → 队列满 → max线程满 → 拒绝
  • Executors.newFixedThreadPool 无界队列 = 生产禁忌
  • 线程一定要命名(ThreadFactory),监控 rejected 指标
  • 一句话:「线程池隔离防雪崩,拒绝策略看业务——重要任务反压,非关键任务丢弃,所有策略必须告警」

状态

  • 已背速记
  • 能讲通俗版
  • 能答线程池参数调优追问