线程池参数怎么配?为什么阿里规约不让用 Executors?
Executors.newFixedThreadPool() 一行就能建池,但《阿里巴巴 Java 开发手册》明确禁止。
原因不是"它写得不好",而是它把两个关键参数藏起来了:队列容量和最大线程数。
1. 先看清 Executors 藏了什么
| 工厂方法 | 核心线程 | 最大线程 | 队列 | 隐患 |
|---|---|---|---|---|
newFixedThreadPool(n) | n | n | LinkedBlockingQueue(无界) | 任务堆积 → OOM |
newSingleThreadExecutor() | 1 | 1 | LinkedBlockingQueue(无界) | 同上 |
newCachedThreadPool() | 0 | Integer.MAX_VALUE | SynchronousQueue | 线程数爆炸 → OOM / 上下文切换 |
newScheduledThreadPool(n) | n | Integer.MAX_VALUE | DelayedWorkQueue | 同上 |
两个致命点:
- 无界队列:
maximumPoolSize永远不会生效(队列不满就不会扩容),任务无限堆积直到 OOM; - 无界最大线程数:
CachedThreadPool在流量突增时会创建成千上万个线程,直接把机器打挂。
2. 七个参数怎么定
new ThreadPoolExecutor(
corePoolSize, // 常驻线程数
maximumPoolSize, // 峰值线程数
keepAliveTime, // 非核心线程空闲存活时间
TimeUnit.SECONDS,
workQueue, // 有界队列
threadFactory, // 自定义线程名,便于排查
rejectedHandler // 拒绝策略
);
2.1 线程数:先分清任务类型
| 任务类型 | 经验公式 | 说明 |
|---|---|---|
| CPU 密集 | N + 1 | N = 核数;多 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); }
}
三个细节:
- 给线程起名(
biz-pool-*):出问题时jstack里一眼能看出是哪个池; allowCoreThreadTimeOut(true):低峰期核心线程也回收,适合流量波动大的服务;- 池要单例、可观测:暴露
getQueue().size()、getActiveCount()到监控。
4. 怎么判断池子不够用
看四个指标:
| 指标 | 含义 | 异常信号 |
|---|---|---|
activeCount | 正在干活的线程 | 长期 = maxPoolSize |
queue.size() | 排队任务数 | 持续增长不下降 |
| 任务等待时间 | 从提交到开始执行 | 超过业务容忍阈值 |
| 拒绝次数 | 拒绝策略触发次数 | 任何非零都值得告警 |
只加线程不扩队列,或只扩队列不加线程,都是治标。真正要问的是:为什么任务处理变慢了? (下游 RT 变高、锁竞争、DB 慢查询——线程池只是把问题放大呈现出来。)
5. 线程池隔离:别用一个池干所有事
- 核心链路(下单、支付)与旁路(发短信、写日志)分开池;
- 旁路池满了不能影响核心链路——这是"故障不扩散"的基本要求;
- Spring 里用
@Async("poolName")指定池,不要用默认的SimpleAsyncTaskExecutor(每次新建线程,等于没池)。
小结
禁止 Executors 的实质是:它替你做了两个危险决定(无界队列、无界线程数)。
自己 new ThreadPoolExecutor 并填满七个参数,看似繁琐,其实是逼你想清楚三件事:
峰值能排多少队、排满了怎么拒绝、出问题时怎么观测。