跳到主要内容

JVM 线程池扩容机制

· 阅读需 4 分钟

HotSpot VM 的线程模型中,Java 线程与操作系统线程是一对一映射关系(JDK19 之后虽然引入了虚拟线程,但传统线程仍然是这种模型)。也就是说,一个 Java 线程会对应一个操作系统内核线程(KLT / LWP)。

当 Java 创建线程时,需要调用操作系统内核 API 创建对应的内核线程,操作系统需要为其分配栈空间、调度信息等资源;当 Java 线程结束时,对应的内核线程也会被回收。因此线程创建和销毁成本较高,线程数量也不能无限增加

当线程数量过多时:

  1. 线程创建会带来较高的系统开销
  2. CPU 在多个线程之间频繁进行 上下文切换(Context Switch)
  3. 大量线程可能导致 系统性能下降甚至 OOM

因此,在实际开发中通常使用 线程池(ThreadPoolExecutor) 来统一管理线程生命周期。

ThreadPoolExecutor 核心参数

参数类型说明
corePoolSizeint核心线程数,线程池中长期保留的线程数量
maximumPoolSizeint最大线程数,当任务队列满时允许创建的最大线程数量
keepAliveTimelong非核心线程的最大空闲存活时间
unitTimeUnitkeepAliveTime 的时间单位
workQueueBlockingQueue任务队列,用于存放等待执行的任务
threadFactoryThreadFactory线程创建工厂,用于自定义线程名称、优先级等
handlerRejectedExecutionHandler拒绝策略,当任务无法被执行时的处理方式

线程池的任务处理流程

当有新的任务提交到线程池时,线程池会按照以下顺序处理。

1)先使用核心线程执行任务

如果当前运行线程数 小于 corePoolSize,线程池会直接创建新线程执行任务。

2)核心线程已满 → 任务进入队列

如果当前线程数 已经达到 corePoolSize,新任务不会立即触发线程创建,而是进入 BlockingQueue 任务队列 等待执行。

3)队列已满 → 扩容线程池

如果任务队列已经达到最大容量,且当前线程数 小于 maximumPoolSize,此时线程池会 创建新的线程处理任务。这些线程称为 非核心线程

4)超过最大线程数 → 拒绝策略

如果队列已满,且当前线程数已经达到 maximumPoolSize,此时线程池会触发 RejectedExecutionHandler 拒绝策略

常见策略:

  • AbortPolicy(默认,直接抛异常)
  • CallerRunsPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

keepAliveTime 的作用

当线程池中的 非核心线程keepAliveTime 时间内 没有执行任何任务 时,该线程会被销毁。因此核心线程默认不会销毁,非核心线程空闲超过 keepAliveTime 会被回收。这样线程池可以在 高峰期扩容,在低负载时回收线程

一个非常容易踩的坑

如果 workQueue 使用 无界队列(如 LinkedBlockingQueue 默认构造),任务会一直进入队列,队列几乎不会满。于是线程池永远走不到扩容那一步,maximumPoolSizekeepAliveTime 基本不会生效。这也是很多线上系统 线程池配置失效 的常见原因。

小结

线程池的扩容路径可以概括为:核心线程 → 任务队列 → 非核心线程 → 拒绝策略。maximumPoolSize 只有在队列满之后才会生效,keepAliveTime 则决定了非核心线程的回收时机。配置线程池时,队列类型的选择和线程数参数同样重要,无界队列会让扩容与拒绝策略形同虚设。理解这条完整链路,线上"线程池参数不生效"的大多数问题就有了答案。

评论 / COMMENTS