JVM 线程池扩容机制
在 HotSpot VM 的线程模型中,Java 线程与操作系统线程是一对一映射关系(JDK19 之后虽然引入了虚拟线程,但传统线程仍然是这种模型)。也就是说,一个 Java 线程会对应一个操作系统内核线程(KLT / LWP)。
当 Java 创建线程时,需要调用操作系统内核 API 创建对应的内核线程,操作系统需要为其分配栈空间、调度信息等资源;当 Java 线程结束时,对应的内核线程也会被回收。因此线程创建和销毁成本较高,线程数量也不能无限增加。
当线程数量过多时:
- 线程创建会带来较高的系统开销
- CPU 在多个线程之间频繁进行 上下文切换(Context Switch)
- 大量线程可能导致 系统性能下降甚至 OOM
因此,在实际开发中通常使用 线程池(ThreadPoolExecutor) 来统一管理线程生命周期。
ThreadPoolExecutor 核心参数
| 参数 | 类型 | 说明 |
|---|---|---|
| corePoolSize | int | 核心线程数,线程池中长期保留的线程数量 |
| maximumPoolSize | int | 最大线程数,当任务队列满时允许创建的最大线程数量 |
| keepAliveTime | long | 非核心线程的最大空闲存活时间 |
| unit | TimeUnit | keepAliveTime 的时间单位 |
| workQueue | BlockingQueue | 任务队列,用于存放等待执行的任务 |
| threadFactory | ThreadFactory | 线程创建工厂,用于自定义线程名称、优先级等 |
| handler | RejectedExecutionHandler | 拒绝策略,当任务无法被执行时的处理方式 |
线程池的任务处理流程
当有新的任务提交到线程池时,线程池会按照以下顺序处理。
1)先使用核心线程执行任务
如果当前运行线程数 小于 corePoolSize,线程池会直接创建新线程执行任务。
2)核心线程已满 → 任务进入队列
如果当前线程数 已经达到 corePoolSize,新任务不会立即触发线程创建,而是进入 BlockingQueue 任务队列 等待执行。
3)队列已满 → 扩容线程池
如果任务队列已经达到最大容量,且当前线程数 小于 maximumPoolSize,此时线程池会 创建新的线程处理任务。这些线程称为 非核心线程。
4)超过最大线程数 → 拒绝策略
如果队列已满,且当前线程数已经达到 maximumPoolSize,此时线程池会触发 RejectedExecutionHandler 拒绝策略。
常见策略:
- AbortPolicy(默认,直接抛异常)
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
keepAliveTime 的作用
当线程池中的 非核心线程在 keepAliveTime 时间内 没有执行任何任务 时,该线程会被销毁。因此核心线程默认不会销毁,非核心线程空闲超过 keepAliveTime 会被回收。这样线程池可以在 高峰期扩容,在低负载时回收线程。
一个非常容易踩的坑
如果 workQueue 使用 无界队列(如 LinkedBlockingQueue 默认构造),任务会一直进入队列,队列几乎不会满。于是线程池永远走不到扩容那一步,maximumPoolSize 和 keepAliveTime 基本不会生效。这也是很多线上系统 线程池配置失效 的常见原因。
小结
线程池的扩容路径可以概括为:核心线程 → 任务队列 → 非核心线程 → 拒绝策略。maximumPoolSize 只有在队列满之后才会生效,keepAliveTime 则决定了非核心线程的回收时机。配置线程池时,队列类型的选择和线程数参数同样重要,无界队列会让扩容与拒绝策略形同虚设。理解这条完整链路,线上"线程池参数不生效"的大多数问题就有了答案。
评论 / COMMENTS