线程池参数怎么配?为什么阿里规约不让用 Executors?

#Java#面试八股

线程池参数怎么配?为什么阿里规约不让用 Executors?

Executors.newFixedThreadPool() 一行就能建池,但《阿里巴巴 Java 开发手册》明确禁止。 原因不是"它写得不好",而是它把两个关键参数藏起来了:队列容量和最大线程数。

1. 先看清 Executors 藏了什么

工厂方法核心线程最大线程队列隐患
newFixedThreadPool(n)nnLinkedBlockingQueue(无界)任务堆积 → OOM
newSingleThreadExecutor()11LinkedBlockingQueue(无界)同上
newCachedThreadPool()0Integer.MAX_VALUESynchronousQueue线程数爆炸 → OOM / 上下文切换
newScheduledThreadPool(n)nInteger.MAX_VALUEDelayedWorkQueue同上

两个致命点:

  • 无界队列:maximumPoolSize 永远不会生效(队列不满就不会扩容),任务无限堆积直到 OOM;
  • 无界最大线程数:CachedThreadPool 在流量突增时会创建成千上万个线程,直接把机器打挂。

2. 七个参数怎么定

new ThreadPoolExecutor(
    corePoolSize,        // 常驻线程数
    maximumPoolSize,     // 峰值线程数
    keepAliveTime,       // 非核心线程空闲存活时间
    TimeUnit.SECONDS,
    workQueue,           // 有界队列
    threadFactory,       // 自定义线程名,便于排查
    rejectedHandler      // 拒绝策略
);

2.1 线程数:先分清任务类型

任务类型经验公式说明
CPU 密集N + 1N = 核数;多 1 个用于偶尔的页缺失
IO 密集N × (1 + 等待时间/计算时间)等待占比越高,线程越多

实践里更稳的做法:先按公式给初值,再用压测调。线程数不是算出来的,是测出来的—— 因为"等待时间占比"随依赖(DB、下游 RPC)的 RT 变化。

2.2 队列:必须有界

new LinkedBlockingQueue<>(1000)   // 有界,队列满即触发拒绝策略

队列长度是缓冲能力:太短 → 高峰直接拒绝;太长 → 排队时间超时、用户等到的结果是失败。 通常按"能容忍的排队时间 × QPS"估算,比如 200 QPS、容忍 5 秒 → 1000。

2.3 拒绝策略:默认的那个最危险

默认 AbortPolicy 抛 RejectedExecutionException——如果调用方没 catch,请求直接 500。 四种策略:

策略行为适用
AbortPolicy(默认)抛异常必须让调用方感知失败
CallerRunsPolicy调用线程自己执行天然背压(提交者被拖慢)
DiscardPolicy静默丢弃几乎不用(排查困难)
DiscardOldestPolicy丢最老的再提交只保留最新任务(如行情推送)

CallerRunsPolicy 是被低估的选择:它让提交任务的一方自己执行,自然限速, 不会丢任务也不会 OOM,代价是提交线程被占住。

3. 一个可用的模板

public class BizExecutor {
    private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
        8, 16, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(2000),
        r -> {
            Thread t = new Thread(r, "biz-pool-" + r.hashCode());
            t.setDaemon(true);
            return t;
        },
        new ThreadPoolExecutor.CallerRunsPolicy());

    static {
        POOL.allowCoreThreadTimeOut(true); // 低峰期回收核心线程,省资源
    }

    public static void submit(Runnable task) { POOL.execute(task); }
}

三个细节:

  1. 给线程起名(biz-pool-*):出问题时 jstack 里一眼能看出是哪个池;
  2. allowCoreThreadTimeOut(true):低峰期核心线程也回收,适合流量波动大的服务;
  3. 池要单例、可观测:暴露 getQueue().size()、getActiveCount() 到监控。

4. 怎么判断池子不够用

看四个指标:

指标含义异常信号
activeCount正在干活的线程长期 = maxPoolSize
queue.size()排队任务数持续增长不下降
任务等待时间从提交到开始执行超过业务容忍阈值
拒绝次数拒绝策略触发次数任何非零都值得告警

只加线程不扩队列,或只扩队列不加线程,都是治标。真正要问的是:为什么任务处理变慢了? (下游 RT 变高、锁竞争、DB 慢查询——线程池只是把问题放大呈现出来。)

5. 线程池隔离:别用一个池干所有事

  • 核心链路(下单、支付)与旁路(发短信、写日志)分开池;
  • 旁路池满了不能影响核心链路——这是"故障不扩散"的基本要求;
  • Spring 里用 @Async("poolName") 指定池,不要用默认的 SimpleAsyncTaskExecutor(每次新建线程,等于没池)。

小结

禁止 Executors 的实质是:它替你做了两个危险决定(无界队列、无界线程数)。 自己 new ThreadPoolExecutor 并填满七个参数,看似繁琐,其实是逼你想清楚三件事: 峰值能排多少队、排满了怎么拒绝、出问题时怎么观测。

笔记加载中…