
我刚开始学习java的那几年,对线程池没什么感觉。但实际上在线上系统里,线程池是比锁、比 MQ 都更容易装死的东西。很多人以为 new 一个就完事,结果高峰期直接把 CPU 干到 800% ,队列堆到几千条,线程一直创建不下来。我把这几年踩过的坑、救过的火、修过的线程池都写进这篇文章里,希望你看完后能真的写出一个适合自己业务的线程池,而不是工具类复制粘贴。
Java 的线程池结构大概是这样:
听着简单,但线程池的行为是被几个参数联动控制的,一改一个命。下面这些参数,你要真搞懂,线程池基本就通了。
核心线程数量就是你的“起步并发能力”。我踩过一次特别蠢的坑就是:我们有个风控接口,计算量特别轻,大部分都是查询。结果我把 corePoolSize 设置得特别大,32 个核心线程。那段时间 CPU 飙得很莫名其妙。最后才发现:核心线程多到离谱,线程上下文切换比业务逻辑都耗时。
实际业务平均 RT 是 10ms 左右,给 8~10 个线程就够了。有同事问我,怎么估核心线程数量?我一般懒得算太复杂,用最简单的经验公式:
CPU 密集型:核心线程 ≈ CPU 核数
IO 密集型:核心线程 ≈ CPU 核数 * 2 或更多别死算,但至少别瞎设。
这个值决定你愿意让线程池撑到什么程度。我踩过一个坑是把 maxPoolSize 设置得太小。
那次我们一个接口发生毛刺峰值(QPS 平时 300,那天突然冲到 900+),核心线程都满了、队列堆满了、maxPoolSize 不够,线程池直接把请求往 “拒绝策略” 里扔。
最后导致用户端各种“网络繁忙,请稍后再试”。从那次以后,我意识到要合理使用设置maxPoolSize的大小,避免主机负载过高,一般会设成 corePoolSize 的 2~4 倍,但一定要结合监控。
队列是个太多人不重视的参数。队列无限大,等于慢性自杀。我们曾经有个服务把队列设成 LinkedBlockingQueue(Integer.MAX_VALUE)。结果某个接口被外部系统打了 10 倍流量,线程处理不过来,队列一下堆了 近 20 万 条任务。整个服务像中暑了一样:
后来我总结了一句话:队列不是用来塞无限任务的,是用来撑住瞬时波动的。能接受的队列大小,一般看接口 RT 和平均并发。比如 RT 50ms,平均 QPS 200,那 10% 的余量就是个 1000 级别的队列。
大部分业务可以把它设得稍微小一点,让临时线程尽快关闭。除非你的流量波动巨大(比如某些活动场景),否则留太久也只会浪费资源。我习惯的设置是:
IO 密集:30 秒左右
CPU 密集:10 秒左右Java 内置四种拒绝策略:
适合那些“必须强一致”的接口,比如:
你宁愿失败,也不愿排队排出 10 秒。但别忘了 catch,不然你会在线上看到满屏堆栈日志。
我基本不用,风险太高。但有一种情况会用:
“非核心链路”、“可丢数据” 的异步任务,比如埋点日志。
丢了就丢了,用户感知不到。
这在高峰期有时很有用。某次我们有个“延迟同步用户资料”的任务堆积了,堆积原因是某第三方接口 RT 不稳定。业务逻辑是:后来的任务一般比旧的更有意义(用户最新资料 >旧资料)。所以选这种策略刚好合适。
这个策略有点“自保”意味。当线程池满了就让调用线程处理任务,从而让系统整体节奏降下来。我在压测接口时经常用它。好处是很明显:
坏处也明显:
所以它不是万能的,但非常好用。
第一次做线程池隔离的时候,我犯了一个致命错误:
所有接口都用一个线程池。
结果是某个 RT 爆炸的接口把线程池占满了,其他接口直接被“绑架”。那次我记得非常清楚:晚上 11 点半,我一个人在会议室被领导围观修复。自那之后,我的线程池策略是这样的:
比如:
举例:
core 20
max 100
queue 5000core = CPU 核数
max=CPU 核数 + 1 或 2
queue=100~500你不可能用一样的线程池去做完全不同的任务。
代表任务:
关键点:无论你开 100 个线程,它们也要排队等 CPU。
所以线程池设大了完全没意义。
推荐:
core = CPU 核心数
max = CPU 核心数 + 2代表任务:
线程花大量时间在等待上,所以可以多开一些。
推荐:
core = CPU 核数 * 2
max = CPU 核数 * 3~5但注意:如果队列太大,会造成请求延迟无限堆积,导致排队时间比执行时间还长。
很多同事以为 ScheduledThreadPoolExecutor 和普通线程池差不多,其实差别很大。
而且它的任务可能因为执行时间长导致“任务堆叠”。
我有一次遇到一个定时任务:
解决方法:
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(4);
executor.setRemoveOnCancelPolicy(true);
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);或者干脆用 XXL-JOB / Quartz,不要自己乱写。
一个“不会监控线程池”的系统,是迟早会炸的。
我最关注三个指标:
如果一直接近 maxPoolSize,说明池子太小或业务 RT 太长。
我会在 Prometheus 里加一个报警:
队列连续 1 分钟 > 80% 容量 → 报警
可以在 wrapper 中记录:
long start = System.currentTimeMillis();
try {
runnable.run();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 200) log.warn("任务耗时过长: {} ms", cost);
}这比单纯看 RT 有意义得多。
当时我们搞一个风控缓存刷新任务,每分钟几十个任务,不多。结果某天 Redis 异常,刷新任务卡在 IO 上,队列一路上涨到 5 万条。线程池没报错,但系统就是迟迟不处理,像是“半死不活”。
教训:
大队列不是保险,是隐患。 让系统早点抛错,远比堆成一座山靠谱。
现在的处理方式:
队列上限 1000
超过就走 CallerRunsPolicy把问题暴露在可控范围。
这个是我在多个项目实战验证过的线程池模板,
特点:
public class BizThreadPool {
public static ThreadPoolExecutor create(
String name,
int core,
int max,
int queueSize) {
return new ThreadPoolExecutor(
core,
max,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueSize),
new NamedThreadFactory(name),
new RejectedHandler(name)
) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
r = wrap(r);
super.beforeExecute(t, r);
}
};
}
private static Runnable wrap(Runnable runnable) {
return () -> {
long start = System.currentTimeMillis();
try {
runnable.run();
} catch (Exception e) {
log.error("线程池任务执行异常", e);
throw e;
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 200)
log.warn("任务执行耗时过长:{} ms", cost);
}
};
}
static class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger index = new AtomicInteger(1);
private final String name;
NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, name + "-" + index.getAndIncrement());
}
}
static class RejectedHandler implements RejectedExecutionHandler {
private final String name;
public RejectedHandler(String name) {
this.name = name;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.error("线程池[{}]已满:active={}, queue={}", name,
e.getActiveCount(), e.getQueue().size());
throw new RejectedExecutionException("线程池满:" + name);
}
}
}ThreadPoolExecutor executor =
BizThreadPool.create("order-sync", 8, 32, 2000);
executor.execute(() -> {
// 业务逻辑
});这个线程池在我们公司至少支撑过:
你可以根据业务轻松扩展。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。