4. keepAliveTime 参数详解与最佳实践:非核心线程的存活时间

线程池里的线程,不能只创建不回收吧?
keepAliveTime 这个参数,就是用来控制非核心线程“闲着多久才被销毁”的。

我刚开始接触线程池时,对这个参数很不在意。总觉得设大点、设小点无所谓。
直到有一次线上服务 CPU 忽高忽低,线程数像过山车一样上下波动……
嗯,从那以后我才认真研究起这个参数来。

4.1 keepAliveTime 到底在管什么?

说白了,这个参数只针对“非核心线程”生效。
当线程池里的线程数超过了 corePoolSize,多出来的线程就是“非核心”的。
这些线程如果空闲时间超过了 keepAliveTime,就会被回收掉。

来看个例子:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,          // 核心线程数
    10,         // 最大线程数
    30,         // 非核心线程空闲等待时间
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100)
);

上面这段代码,核心线程 5 个,最大允许 10 个。
当第 6 个任务进来时,会创建第 6 个线程——这个就是非核心线程。
如果这个线程空闲了 30 秒,那就拜拜了。

关键点: keepAliveTime 只针对非核心线程,核心线程默认不会被回收。
除非你调用了 allowCoreThreadTimeOut(true)。

4.2 设置依据:不是拍脑袋就能定的

这个值到底设多少?
不能太短,也不能太长。我踩过的坑就在这里。

设置倾向 后果 典型场景
太短(如 1 秒) 线程频繁创建销毁,系统开销大 突发性任务,间隔较长
太长(如 10 分钟) 空闲线程占内存,浪费资源 任务稀少,但单次执行时间长
适中的(如 30~60 秒) 既能复用线程,又不浪费资源 大部分业务场景

我个人习惯这样判断:
先看看任务到达的平均间隔时间。
如果任务平均每 5 秒来一个,keepAliveTime 设个 10~15 秒就够用。
如果任务平均每分钟才来一个,那设 30 秒也行,甚至直接用核心线程就够了。

4.3 如何避免频繁创建销毁

这个问题的本质,其实是“不要让线程刚创建就被回收”。
你想想看,如果一个任务来了,创建了一个线程,处理了 50 毫秒就完事了。
结果紧接着又来一个任务,又要创建线程……

为什么会这样?
因为 keepAliveTime 设得太短了!
线程刚完成任务,还没来得及等下一个任务,就被回收了。

避坑指南:
我曾经在一个高并发场景下,把 keepAliveTime 设成了 2 秒。
结果线上日志显示每秒都有几十次线程创建和销毁。
CPU 的上下文切换开销比处理任务还高。

后来我改成 30 秒,线程创建频率直接降为 0。
核心线程已经够用了,非核心线程几乎没创建过。

4.3.1 加一点“缓冲”效果

我的建议是:
把 keepAliveTime 设为任务执行时间均值的 2~5 倍。
这样即使任务偶有波动,线程也能留下来等一等。

比如,每个任务平均执行 200 毫秒,那 keepAliveTime 设 1~2 秒就差不多。

4.3.2 配合核心线程数一起调

如果核心线程数已经能覆盖大部分并发量,非核心线程很少用上。
那 keepAliveTime 设大一点也无伤大雅。
反正非核心线程压根不创建,回收个啥?

但如果核心线程数设得比较少,比如 2 个,任务又密集。
那 keepAliveTime 就得仔细算算了。
设置太短,非核心线程会在“创建→执行→回收”之间来回跑。

注意: 频繁的线程创建和销毁,每次都要经历系统调用。
这会带来不小的性能开销,尤其在 Linux 上,线程创建的成本比 Java 对象创建高得多。

4.4 核心线程也能超时?

刚才说过,默认情况下核心线程是不会被回收的。
但有的场景下,比如夜间流量很低,你还让 10 个核心线程闲着?
不想浪费内存,怎么办?

ThreadPoolExecutor 提供了一个方法:

executor.allowCoreThreadTimeOut(true);

打开这个开关后,核心线程也会受 keepAliveTime 控制。
空闲超过指定时间,一样被回收。

我什么时候会用?
比如定时任务线程池,白天忙、晚上闲。
我会设置 allowCoreThreadTimeOut(true),并给一个合理的 keepAliveTime。
晚上空闲时线程自动回收,白天任务来了再创建。

提示: 如果设置了 allowCoreThreadTimeOut(true),keepAliveTime 必须大于 0。
否则会抛出 IllegalArgumentException。

4.5 一张图理解 keepAliveTime 的决策逻辑

keepAliveTime 决策逻辑流程 任务提交到线程池 当前线程数 > corePoolSize? (是否使用了非核心线程) 否 核心线程处理任务 keepAliveTime 不生效 是 使用非核心线程 非核心线程处理任务 线程开始计时等待 空闲时间 > keepAliveTime? 否 继续等待 是 销毁非核心线程 关键原则 keepAliveTime 设得太短 → 线程频繁创建销毁 设得太长 → 空闲线程浪费资源

4.6 我的 keepAliveTime 实践建议

说了这么多,给个硬核结论吧:

  • 普通业务接口: 30~60 秒。任务间隔一般在秒级,留够缓冲。
  • 定时任务/批量处理: 如果任务很密集,几分钟或者干脆不用非核心线程。
  • 流量有明显波峰波谷: 配合 allowCoreThreadTimeOut(true),设 60~120 秒。
  • 内存敏感的场景: 设短一点,10~15 秒。别让空闲线程占着内存不放。
小技巧: 如果真的拿不准,先设个 60 秒跑一天看看。
观察线程池的 activeCount、poolSize、completedTaskCount 这几个指标。
如果非核心线程一直在创建销毁,那就调大 keepAliveTime。
如果非核心线程很少用到,调小一点也没事。

最后说一句:
keepAliveTime 不是一个孤立参数。它跟 corePoolSize、maximumPoolSize、workQueue 是联动的。
调这个参数时,最好一眼扫过去,看看线程池的整体水位。