线程池是 java 并发编程里最常用也最容易用错的工具之一。用好了,系统吞吐量翻倍;用错了,线上 oom、任务丢失、线程暴涨……踩坑的代价往往很高。
这篇文章从核心参数讲起,再到拒绝策略怎么选,最后把生产环境里最常见的坑逐个拆开说清楚。
一、线程池核心参数详解
threadpoolexecutor 是 java 线程池的核心实现,它的构造方法长这样:
public threadpoolexecutor(
int corepoolsize,
int maximumpoolsize,
long keepalivetime,
timeunit unit,
blockingqueue<runnable> workqueue,
threadfactory threadfactory,
rejectedexecutionhandler handler
)
七个参数,逐个拆开看。
1. corepoolsize(核心线程数)
线程池长期维持的线程数量。即使这些线程空闲,默认也不会被回收。
很多人以为"线程池里的线程数会随任务量自动伸缩",其实核心线程默认是常驻的——除非你调用了 allowcorethreadtimeout(true)。
怎么设?
- cpu 密集型任务(如计算、加解密):
corepoolsize = cpu 核数 ± 1 - io 密集型任务(如数据库访问、rpc 调用):
corepoolsize = cpu 核数 × (1 + 平均等待时间 / 平均计算时间),一般 2×~4× 核数
int corepoolsize = runtime.getruntime().availableprocessors() * 2;
2. maximumpoolsize(最大线程数)
线程池最多能创建的线程数。当任务队列满了之后,线程池会继续创建线程,直到达到这个上限。
关键理解:线程数什么时候会从 core 涨到 max?
不是任务一多就涨。实际流程是:
提交任务 → 核心线程没满?→ 创建核心线程执行
→ 核心线程满了?→ 任务进队列
→ 队列满了?→ 创建非核心线程执行
→ 线程数达到 max?→ 触发拒绝策略
所以 队列的大小直接决定了非核心线程什么时候被创建。如果用了无界队列,非核心线程永远不会被创建,maximumpoolsize 形同虚设。
3. keepalivetime + unit(线程存活时间)
非核心线程空闲超过这个时间就会被回收。默认只对非核心线程生效,但可以通过 allowcorethreadtimeout(true) 让核心线程也遵守这个规则。
生产建议:对于流量波动大的服务,建议开启核心线程超时回收,避免低峰期空占资源。
4. workqueue(任务队列)
这是最容易踩坑的参数。常见选择:
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
linkedblockingqueue | 默认无界,可能 oom | 不推荐直接使用 |
arrayblockingqueue | 有界,需指定容量 | 大多数场景 |
synchronousqueue | 不存任务,直接交付 | 高吞吐、低延迟 |
priorityblockingqueue | 优先级队列 | 任务有优先级区分 |
重点提醒:executors.newfixedthreadpool() 和 executors.newsinglethreadexecutor() 内部用的是无界 linkedblockingqueue,任务堆积时直接 oom。这就是为什么阿里 java 规约禁止使用 executors 创建线程池。
5. threadfactory(线程工厂)
用于创建线程,可以自定义线程名、优先级、是否为守护线程等。
threadfactory factory = new threadfactory() {
private final atomicinteger count = new atomicinteger(1);
public thread newthread(runnable r) {
return new thread(r, "biz-pool-" + count.getandincrement());
}
};
生产价值:自定义线程名让排查问题时能一眼看出是哪个线程池的线程在干活,而不是看到 pool-1-thread-3 一脸懵。
6. handler(拒绝策略)
任务提交失败时的处理方式,下文单独详讲。
二、拒绝策略怎么选
当线程数达到 maximumpoolsize 且队列也满了,新提交的任务会触发拒绝策略。
jdk 内置了四种:
1. abortpolicy(默认)—— 直接抛异常
new threadpoolexecutor.abortpolicy()
抛出 rejectedexecutionexception,中断调用者线程。
适用场景:任务不能丢,调用方需要感知失败并重试。大多数在线业务选这个。
2. callerrunspolicy —— 调用者自己跑
new threadpoolexecutor.callerrunspolicy()
不让线程池跑,让提交任务的线程自己执行这个任务。
适用场景:任务不能丢,且能接受提交方被阻塞。相当于一种天然的背压机制——提交方跑慢了,生产速度自然就降下来了。适合异步化但要求最终执行的场景。
注意:如果提交方是 tomcat 的 http 工作线程,用了这个策略可能导致 http 请求被阻塞,连接池耗尽。要慎重。
3. discardpolicy —— 直接丢弃
new threadpoolexecutor.discardpolicy()
啥也不干,任务静默丢失。
适用场景:任务可丢,比如非核心的埋点上报、日志收集。但生产环境慎用,出了问题很难排查。
4. discardoldestpolicy —— 丢弃队列里最老的任务
new threadpoolexecutor.discardoldestpolicy()
丢弃队列头部的任务(等待最久的那个),然后尝试重新提交当前任务。
适用场景:任务有时效性,旧任务丢了无所谓,新任务更重要。比如实时价格推送。
5. 自定义拒绝策略
生产环境最推荐的方式——记录日志、报警、降级,然后决定怎么处理:
new rejectedexecutionhandler() {
@override
public void rejectedexecution(runnable r, threadpoolexecutor executor) {
// 1. 记录被拒绝的任务信息
log.warn("task rejected, pool status: active={}, queue={}",
executor.getactivecount(), executor.getqueue().size());
// 2. 上报监控
metrics.counter("thread_pool_rejected").increment();
// 3. 写入降级存储(如 mq、redis),后续补偿
fallbackstorage.put(r);
// 4. 或者抛异常让调用方感知
throw new rejectedexecutionexception("task rejected, please retry");
}
};
拒绝策略选择决策树
任务能不能丢?
├── 不能丢 → 调用方能重试吗?
│ ├── 能 → abortpolicy + 重试机制
│ └── 不能 → callerrunspolicy(注意背压影响)
└── 可以丢 → 丢哪个?
├── 都行 → discardpolicy + 监控告警
└── 保新的 → discardoldestpolicy
三、生产避坑指南
坑 1:用 executors 创建线程池
// ❌ 危险!无界队列,可能 oom executors.newfixedthreadpool(10); executors.newcachedthreadpool(); // 最大线程数 integer.max_value
// ✅ 正确:手动创建,明确参数
new threadpoolexecutor(
10, 50,
60l, timeunit.seconds,
new arrayblockingqueue<>(1000),
new namedthreadfactory("biz-pool"),
new threadpoolexecutor.abortpolicy()
);
坑 2:队列设置无界
无界队列 = 内存泄漏的定时炸弹。任务生产速度大于消费速度时,队列无限增长,直到 full gc 甚至 oom。
正确做法:根据内存和任务大小估算队列容量,配合拒绝策略形成闭环。
// 估算:单个任务对象约 1kb,队列最多占 100mb → 容量设 100000 new arrayblockingqueue<>(100_000);
坑 3:所有任务共用一个线程池
一个系统中如果 io 密集和 cpu 密集任务混在一个池子里,cpu 密集任务会拖慢整个池子,io 密集任务占满线程又会让 cpu 密集任务得不到执行。
正确做法:按业务隔离,不同性质的任务用不同的线程池。
// 计算任务池
threadpoolexecutor computepool = new threadpoolexecutor(
cores, cores * 2, 60, seconds,
new arrayblockingqueue<>(200), ...);
// io 任务池
threadpoolexecutor iopool = new threadpoolexecutor(
cores * 4, cores * 8, 60, seconds,
new arrayblockingqueue<>(2000), ...);
坑 4:忘了关闭线程池
应用关闭时如果线程池没 shutdown(),非守护线程会阻止 jvm 退出,导致优雅停机失败。
// 应用关闭钩子中
executor.shutdown();
try {
if (!executor.awaittermination(30, timeunit.seconds)) {
executor.shutdownnow();
}
} catch (interruptedexception e) {
executor.shutdownnow();
}
spring 环境下,用 @predestroy 或实现 disposablebean 来关闭。
坑 5:线程池参数写死,无法动态调整
线上流量波动时,固定的线程数和队列容量往往不够灵活。
方案:使用支持动态调整的线程池框架,如美团技术团队开源的 dynamictp,或者基于 threadpoolexecutor 的 setcorepoolsize()、setmaximumpoolsize() 方法做运行时调整。
// 运行时动态调整 executor.setcorepoolsize(20); executor.setmaximumpoolsize(100);
坑 6:任务里抛了异常,线程直接挂了
threadpoolexecutor 中如果任务抛出未捕获异常,执行该任务的线程会被销毁,然后线程池创建一个新线程替代。如果异常频繁,线程会不断创建销毁,而且异常信息容易被吞掉。
// ❌ 异常被吞
executor.submit(() -> {
throw new runtimeexception("oops"); // 异常存在 future 里,不 get 就看不到
});
// ✅ 方案 1:用 execute 而不是 submit
executor.execute(() -> {
// 异常会打印到线程的 uncaughtexceptionhandler
});
// ✅ 方案 2:submit + 一定要 get
future<?> future = executor.submit(task);
try {
future.get();
} catch (executionexception e) {
log.error("task failed", e.getcause());
}
// ✅ 方案 3:任务内部自己 try-catch
executor.execute(() -> {
try {
dowork();
} catch (exception e) {
log.error("task error", e);
}
});
坑 7:没有监控
线程池是黑盒,不监控就不知道它什么时候在挣扎。
核心监控指标:
| 指标 | 获取方式 | 告警阈值建议 |
|---|---|---|
| 活跃线程数 | getactivecount() | 持续 > 80% max |
| 队列堆积 | getqueue().size() | 持续 > 70% 容量 |
| 拒绝次数 | 自定义 handler 中计数 | > 0 就告警 |
| 任务完成数 | getcompletedtaskcount() | 突降告警 |
| 线程池大小 | getpoolsize() | 频繁波动 |
接入 micrometer/prometheus 做可视化:
@scheduled(fixedrate = 10000)
public void reportthreadpoolmetrics() {
registry.gauge("threadpool.active", executor.getactivecount());
registry.gauge("threadpool.queue.size", executor.getqueue().size());
registry.gauge("threadpool.pool.size", executor.getpoolsize());
}
坑 8:使用 completablefuture 时默认线程池的陷阱
completablefuture 的 supplyasync() / runasync() 如果不指定线程池,用的是 forkjoinpool.commonpool(),这个池子所有 completablefuture 共享,且线程数 = cpu 核数 - 1。
// ❌ 用公共池,可能互相影响 completablefuture.supplyasync(() -> dosomething()); // ✅ 指定业务线程池 completablefuture.supplyasync(() -> dosomething(), bizexecutor);
四、一个生产级线程池配置模板
@configuration
public class threadpoolconfig {
@bean("biztaskexecutor")
public threadpoolexecutor biztaskexecutor() {
int cores = runtime.getruntime().availableprocessors();
return new threadpoolexecutor(
cores * 2, // corepoolsize
cores * 4, // maximumpoolsize
60l, timeunit.seconds, // keepalivetime
new arrayblockingqueue<>(2000), // workqueue
new threadfactory() { // threadfactory
private final atomicinteger count = new atomicinteger(1);
public thread newthread(runnable r) {
thread t = new thread(r, "biz-worker-" + count.getandincrement());
t.setdaemon(false);
return t;
}
},
new rejectedexecutionhandler() { // handler
@override
public void rejectedexecution(runnable r, threadpoolexecutor executor) {
log.error("biz task rejected! active={}, queue={}, pool={}",
executor.getactivecount(),
executor.getqueue().size(),
executor.getpoolsize());
metrics.counter("biz_pool_rejected").increment();
// 降级:写入延迟队列,后续补偿消费
fallbackqueue.offer(r);
// 或抛异常让调用方感知
throw new rejectedexecutionexception("biz pool overloaded");
}
}
);
}
}
总结
线程池用起来简单,用好很难。记住这几个核心原则:
- 永远手动创建线程池,不用 executors 快捷方法
- 队列必须有界,配合合理的拒绝策略
- 不同业务隔离线程池,避免互相影响
- 异常要处理,submit 要 get,或者任务内部 try-catch
- 监控不能少,队列堆积和拒绝次数是最重要的两个指标
- 参数要能动态调整,线上情况千变万化
线程池的本质是用空间换时间、用排队换削峰。理解了这个本质,参数的选择就不是死记硬背,而是根据业务特征做权衡取舍。
以上就是java中线程池核心参数详解及常见避坑教学的详细内容,更多关于java线程池核心参数的资料请关注代码网其它相关文章!
发表评论