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 | 拒绝策略,当任务无法被执行时的处理方式 |
线程池的任务处理流程
当有新的任务提交到线程池时,线程池会按照以下顺序处理:
先使用核心线程执行任务
如果当前运行线程数 小于 corePoolSize
线程池会直接创建新线程执行任务。
核心线程已满 → 任务进入队列
如果当前线程数 已经达到 corePoolSize:
新任务不会立即创建线程,而是进入 BlockingQueue 任务队列 等待执行。
队列已满 → 扩容线程池
如果:
- 任务队列已经达到最大容量
- 且当前线程数 小于
maximumPoolSize
此时线程池会 创建新的线程处理任务。
这些线程称为 非核心线程。
超过最大线程数 → 拒绝策略
如果:
- 队列已满
- 当前线程数已经达到
maximumPoolSize
此时线程池会触发 RejectedExecutionHandler 拒绝策略。
常见策略:
- AbortPolicy(默认,直接抛异常)
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
keepAliveTime 的作用
当线程池中的 非核心线程在 keepAliveTime 时间内 没有执行任何任务 时,该线程会被销毁。
因此:
- 核心线程:默认不会销毁
- 非核心线程:空闲超过
keepAliveTime会被回收
这样线程池可以在 高峰期扩容,在低负载时回收线程。
一个非常容易踩的坑
如果 workQueue 使用 无界队列(如 LinkedBlockingQueue 默认构造):
- 任务会一直进入队列
- 队列几乎不会满
因此:
- 线程池永远不会扩容
maximumPoolSize和keepAliveTime基本不会生效
这也是很多线上系统 线程池配置失效 的常见原因。