在 Java 的线程池中,线程池数量从 10 变回 5 的场景发生在线程池中临时增加的非核心线程(超过核心线程数的线程)空闲超过指定时间后被回收的情况下。具体来说,这个过程涉及到线程池的线程创建、任务处理和线程回收机制。下面详细分析 Java 线程池的工作原理,特别是线程池数量从 10 变回 5 的场景。
一、线程池工作原理概述
Java 中线程池的实现主要依赖于 ThreadPoolExecutor
类。线程池的两个关键参数是核心线程数(corePoolSize
)和最大线程数(maximumPoolSize
):
- 核心线程数:线程池维护的最小线程数,即使线程空闲也不会被回收。在这里是 5。
- 最大线程数:线程池可以创建的最大线程数,包括核心线程和临时线程。在这里是 10。
线程池的工作原理大致如下:
- 任务提交:当任务被提交到线程池时,如果核心线程数未满,线程池会优先创建新的核心线程处理任务。
- 任务队列:如果核心线程数已满,任务会被放入任务队列等待执行。
- 非核心线程:当核心线程数已满且任务队列也已满,线程池会继续创建新的非核心线程(即超出核心线程数的线程)来处理任务,直到达到最大线程数。
- 线程回收:当线程池的线程数量超过核心线程数,并且这些超出核心线程的线程处于空闲状态超过指定的时间(
keepAliveTime
),这些非核心线程会被逐步回收,直到线程池的线程数减少到核心线程数为止。
二、线程池数量从 10 变回 5 的场景
根据线程池的设计,线程池中的线程数量会动态调整:
- 当有大量任务提交时,线程池会创建额外的线程来处理任务,最多可以扩展到 10 个线程。
- 当任务处理完成,非核心线程处于空闲状态时,经过
keepAliveTime
时间后,线程池会回收非核心线程,逐渐将线程数量降到核心线程数,即 5。
因此,线程池数量从 10 变回 5 的场景发生在以下条件满足的情况下:
- 线程池中的所有任务都已处理完毕,没有新的任务提交。
- 超过核心线程数的 5 个临时线程处于空闲状态,并且空闲时间超过
keepAliveTime
。 - 线程池开始回收这些空闲的临时线程,将线程数量逐步减少到核心线程数 5。
三、线程池数量从 10 变回 5 的详细执行过程
假设核心线程数为 5,最大线程数为 10,keepAliveTime
为 60 秒,任务队列类型为 LinkedBlockingQueue
(容量有限的队列)。
- 初始任务提交:
- 当有任务提交到线程池时,如果当前线程池中线程数量小于 5,线程池会优先创建新的核心线程来处理任务,直到线程数达到 5。
- 如果任务数量继续增加且超过任务队列的容量(任务队列已满),线程池会进一步创建非核心线程(临时线程)来处理这些任务,直到线程池的线程数量达到最大线程数 10。
- 临时线程的创建:
- 在任务高峰期,线程池中的线程数量被扩展到最大线程数 10。
- 此时,线程池中的 5 个核心线程和 5 个非核心线程(临时线程)共同处理任务。
- 任务处理完成,非核心线程进入空闲状态:
- 随着任务处理完毕,线程池中临时增加的非核心线程(即超过核心线程数的 5 个线程)进入空闲状态。
- 非核心线程的空闲计时开始,如果在
keepAliveTime
时间内没有新任务提交到线程池,非核心线程会超时。
- 非核心线程的回收:
- 一旦非核心线程空闲超过
keepAliveTime
(60 秒),线程池会回收这些非核心线程。 ThreadPoolExecutor
通过一个内部的空闲计时器逐步释放空闲的非核心线程。每次检查时,如果检测到某个非核心线程空闲超时,线程池将其从线程池中移除并终止。- 这一过程会持续进行,直到线程池中的线程数量减少到核心线程数 5。
- 一旦非核心线程空闲超过
四、实现原理:线程池如何自动回收非核心线程
在 ThreadPoolExecutor
中,线程池的回收机制是通过 keepAliveTime
和 allowCoreThreadTimeOut
配置来管理的。
- keepAliveTime:设置了非核心线程的最大空闲时间,单位可以是秒、毫秒等。非核心线程空闲超过
keepAliveTime
时会被回收。 - allowCoreThreadTimeOut:默认情况下,核心线程即使空闲也不会被回收。如果将
allowCoreThreadTimeOut
设置为true
,那么核心线程空闲时间超过keepAliveTime
也会被回收。
每个非核心线程在空闲时都会进行空闲时间计时,超过 keepAliveTime
的非核心线程会被逐个回收,从而减少线程池中的线程数量,直到达到核心线程数。
五、代码示例
以下是一个模拟核心线程数为 5,最大线程数为 10,keepAliveTime
为 60 秒的线程池示例。
javaCopy codeimport java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
// 创建线程池,核心线程数为 5,最大线程数为 10,非核心线程的 keepAliveTime 为 60 秒
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
// 提交大量任务使线程池扩展至最大线程数
for (int i = 0; i < 20; i++) {
executor.submit(() -> {
try {
System.out.println("Executing task by " + Thread.currentThread().getName());
Thread.sleep(2000); // 模拟任务处理
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 等待一段时间,确保任务处理完成,非核心线程进入空闲状态
Thread.sleep(3000);
// 查看线程池的当前线程数
System.out.println("Current Pool Size (After tasks): " + executor.getPoolSize());
// 等待超过 keepAliveTime(60 秒),非核心线程将被回收
Thread.sleep(70000); // 等待 70 秒
// 查看线程池的当前线程数,预计会降至核心线程数 5
System.out.println("Current Pool Size (After idle): " + executor.getPoolSize());
// 关闭线程池
executor.shutdown();
}
}
六、总结
Java 线程池的线程数量从 10 变回 5 的过程是通过 keepAliveTime
控制非核心线程的空闲时间。当线程池中的任务减少,非核心线程处于空闲状态且空闲时间超过 keepAliveTime
时,线程池会逐步回收这些空闲的非核心线程,将线程数量减少到核心线程数,从而达到减少资源占用的效果。
4o
You said:
线程池种类有哪些
ChatGPT said:
ChatGPT
Java 提供了多种线程池实现,每种线程池适用于不同的应用场景。主要的线程池类型包括:
- 固定大小线程池(Fixed Thread Pool)
- 使用
Executors.newFixedThreadPool(int nThreads)
创建,线程池的线程数量固定,适用于执行长期任务,减少线程创建的开销。 - 特点:
- 核心线程数和最大线程数相等,线程数固定。
- 任务队列采用无界队列
LinkedBlockingQueue
,当线程都在忙时,新任务会在队列中等待。 - 适用于需要控制并发线程数量,且执行时间较长的任务。
- 使用
- 缓存线程池(Cached Thread Pool)
- 使用
Executors.newCachedThreadPool()
创建,线程数量不固定,根据任务需求动态调整线程数量。 - 特点:
- 核心线程数为 0,最大线程数为
Integer.MAX_VALUE
。 - 任务队列采用
SynchronousQueue
,新任务会直接创建新线程处理,空闲线程超过 60 秒会被回收。 - 适用于短期大量并发任务,不确定线程数的场景。
- 核心线程数为 0,最大线程数为
- 使用
- 单线程线程池(Single Thread Executor)
- 使用
Executors.newSingleThreadExecutor()
创建,线程池中只有一个线程,所有任务按照顺序执行。 - 特点:
- 核心线程数和最大线程数均为 1,任务按顺序执行。
- 保证任务在同一线程中按顺序执行,适用于串行执行任务且需要保持顺序的场景。
- 使用
- 定时线程池(Scheduled Thread Pool)
- 使用
Executors.newScheduledThreadPool(int corePoolSize)
创建,用于执行延迟任务或周期性任务。 - 特点:
- 核心线程数固定,最大线程数为
Integer.MAX_VALUE
。 - 使用
DelayedWorkQueue
任务队列,支持延迟执行和周期性执行。 - 适用于需要定时执行任务或周期性任务的场景,例如定时任务调度。
- 核心线程数固定,最大线程数为
- 使用
- 工作窃取线程池(Work Stealing Pool)
- 使用
Executors.newWorkStealingPool(int parallelism)
创建,基于ForkJoinPool
实现,适合并行计算任务。 - 特点:
- 线程池的线程数等于 CPU 的核心数(默认),可以手动指定。
- 每个线程有自己的任务队列,支持工作窃取(当某线程的队列空闲时可以“窃取”其他线程的任务)。
- 适用于需要提高吞吐量、分而治之的并行计算任务。
- 使用
各种线程池的比较
线程池类型 | 核心线程数 | 最大线程数 | 适用场景 |
---|---|---|---|
Fixed Thread Pool | 固定 | 固定 | 长期任务,控制并发线程数量 |
Cached Thread Pool | 0 | Integer.MAX_VALUE | 短期大量并发任务,适合不确定线程数 |
Single Thread Executor | 1 | 1 | 按顺序执行任务,保持顺序的串行任务 |
Scheduled Thread Pool | 固定 | Integer.MAX_VALUE | 定时任务或周期性任务 |
Work Stealing Pool | CPU 核心数(默认) | Integer.MAX_VALUE | 并行计算,提高吞吐量 |
线程池选择建议
- 固定大小线程池:适合控制并发数的应用,避免过多线程导致系统资源被耗尽。
- 缓存线程池:适合执行短期异步任务,任务量不确定时灵活创建线程。
- 单线程池:适合需要顺序执行的任务,例如日志写入、文件操作。
- 定时线程池:适合周期性执行任务或需要延时执行的场景,例如定时清理缓存、定期备份。
- 工作窃取线程池:适合分而治之的并行计算任务,如数据处理、大型集合计算。
4o
You said:
线程池拒绝策略有哪些?
ChatGPT said:
ChatGPT
在 Java 的 ThreadPoolExecutor
中,当线程池的任务队列已满、线程数量已达到最大线程数,且无法再接受新的任务时,会触发拒绝策略(RejectedExecutionHandler)。拒绝策略用于定义如何处理新提交但无法执行的任务。Java 提供了以下几种内置的拒绝策略:
- AbortPolicy(默认策略)
- 策略说明:直接抛出
RejectedExecutionException
异常,通知调用者任务被拒绝。 - 适用场景:适用于必须执行所有任务的场景,以确保程序知道任务被拒绝,适合无法丢弃任何任务的关键业务场景。
- 代码示例:javaCopy code
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.AbortPolicy() );
- 策略说明:直接抛出
- DiscardPolicy
- 策略说明:直接丢弃无法执行的新任务,不抛出任何异常,也不会通知任务提交方。
- 适用场景:适用于不敏感的任务场景,适合允许丢弃部分任务的情况,例如日志或监控信息的处理等。
- 代码示例:javaCopy code
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.DiscardPolicy() );
- DiscardOldestPolicy
- 策略说明:丢弃任务队列中最旧的任务(即队列头部任务),然后重新尝试提交当前任务。
- 适用场景:适用于需要保证新任务执行优先级的场景,适合队列中的任务相对过时或可以被丢弃的情况。
- 代码示例:javaCopy code
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.DiscardOldestPolicy() );
- CallerRunsPolicy
- 策略说明:由调用线程(即提交任务的线程)直接执行当前任务,避免任务丢失,但会影响调用线程的性能。
- 适用场景:适用于允许任务回退到调用线程执行的情况,可以用于控制提交速率,使得当任务队列满时,调用线程的执行速度减慢,间接减小线程池的负载。
- 代码示例:javaCopy code
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy() );
自定义拒绝策略
除了内置的拒绝策略,ThreadPoolExecutor
允许实现 RejectedExecutionHandler
接口来自定义拒绝策略。例如,可以记录拒绝的任务日志、进行限流控制、触发告警等。
javaCopy codepublic class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑,例如记录日志或告警
System.out.println("Task " + r.toString() + " rejected from " + executor.toString());
}
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new CustomRejectedExecutionHandler()
);
拒绝策略选择建议
- AbortPolicy:适合需要严格控制所有任务执行的场景,通过抛出异常使调用方明确任务未执行。
- DiscardPolicy:适用于不敏感的任务,例如一些临时性、不重要的日志任务。
- DiscardOldestPolicy:适合队列中旧任务可以被丢弃而优先处理新任务的场景,例如频繁更新的监控数据。
- CallerRunsPolicy:适合希望通过调用线程执行以控制任务提交速率的情况,适用于不要求线程池高吞吐量的场景。
4o
You said:
线程池刚创建,没有任务,会新建线程吗?
ChatGPT said:
ChatGPT
当线程池刚创建时,如果没有提交任何任务,那么线程池不会立即创建线程。线程池中的线程创建是按需进行的,即只有在有任务提交并需要线程执行时,线程池才会创建线程。
详细说明
- 核心线程的创建:
- 线程池的核心线程数(
corePoolSize
)定义了线程池在任务队列为空时维持的最小线程数量。 - 但是,核心线程并不会在线程池创建时就立即启动。默认情况下,核心线程是延迟创建的,即只有在有任务提交时,线程池才会按需创建核心线程来执行任务。
- 线程池的核心线程数(
- 线程的按需创建:
- 当任务被提交到线程池时,线程池首先会尝试创建核心线程。如果核心线程数已满,任务会被放入任务队列中。
- 只有当任务队列也满了时,线程池才会继续创建超出核心线程数的非核心线程,直到达到最大线程数(
maximumPoolSize
)。
- 预启动核心线程:
- 尽管线程池默认是按需创建线程,但可以调用
prestartCoreThread()
或prestartAllCoreThreads()
方法来提前启动核心线程。prestartCoreThread()
:启动一个核心线程。prestartAllCoreThreads()
:启动所有核心线程。
- 这样做可以确保在有任务到来前核心线程已经就绪。
- 尽管线程池默认是按需创建线程,但可以调用
示例代码
以下代码展示了默认情况下线程池不会在创建时启动线程,只有在提交任务时才会创建线程。
javaCopy codeimport java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个核心线程数为 2,最大线程数为 4 的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
// 初始时线程池不会创建任何线程
System.out.println("Initial pool size: " + executor.getPoolSize());
// 提交一个任务后,线程池才会创建一个核心线程来执行任务
executor.submit(() -> {
System.out.println("Task is running");
});
// 查看线程池的当前线程数
System.out.println("Pool size after submitting task: " + executor.getPoolSize());
// 关闭线程池
executor.shutdown();
}
}
总结
- 默认情况下,线程池在刚创建时不会立即创建线程。只有当有任务提交时,线程池才会创建线程来处理任务。
- 可以使用
prestartCoreThread()
或prestartAllCoreThreads()
提前创建核心线程,确保线程池在任务到来之前已经有核心线程准备就绪。
4o
You said:
核心线程池数创建之后会一直存在吗?如何保活
ChatGPT said:
ChatGPT
在 Java 的 ThreadPoolExecutor
中,核心线程默认会一直存在,即使它们处于空闲状态,线程池也不会主动回收核心线程。核心线程的存在与线程池的配置密切相关,主要受以下因素影响:
- 核心线程的保活:
- 默认情况下,核心线程在没有任务执行时会保持活动状态,且不会被线程池回收。
- 但是可以通过调用
ThreadPoolExecutor
的方法allowCoreThreadTimeOut(true)
来改变这一行为,使核心线程在空闲时间超过keepAliveTime
后被回收。此时,核心线程的保活行为将与非核心线程相同。
- 核心线程的保活机制:
- 当调用
allowCoreThreadTimeOut(true)
后,核心线程也会受到keepAliveTime
参数的影响。 - 具体来说,如果核心线程在空闲时间内没有被分配到新任务且空闲时间超过
keepAliveTime
,线程池会将该核心线程终止并回收。 - 这种配置适用于一些需要灵活管理资源的场景,例如在低负载时期减少系统资源占用。
- 当调用
- 如何设置核心线程保活:
keepAliveTime
:用于指定线程的最大空闲时间,单位可以是秒、毫秒等,默认为 0。allowCoreThreadTimeOut(true)
:允许核心线程在空闲时间超过keepAliveTime
后回收。
代码示例
以下示例展示了如何配置核心线程的保活时间以及回收机制:
javaCopy codeimport java.util.concurrent.*;
public class CoreThreadKeepAliveExample {
public static void main(String[] args) throws InterruptedException {
// 创建核心线程数为 2,最大线程数为 4 的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
);
// 允许核心线程超时并被回收
executor.allowCoreThreadTimeOut(true);
// 提交两个任务,这将启动核心线程来执行任务
executor.submit(() -> {
System.out.println("Task 1 is running");
});
executor.submit(() -> {
System.out.println("Task 2 is running");
});
// 等待所有任务完成
Thread.sleep(5000);
// 查看当前线程池的核心线程数
System.out.println("Core pool size after tasks: " + executor.getPoolSize());
// 等待超过 keepAliveTime(10 秒),观察核心线程是否被回收
Thread.sleep(15000);
// 查看核心线程数,预计此时会被回收,回到0
System.out.println("Core pool size after idle: " + executor.getPoolSize());
// 关闭线程池
executor.shutdown();
}
}
总结
- 默认情况下,核心线程会一直存在,不会被线程池回收。
- 开启核心线程超时:调用
allowCoreThreadTimeOut(true)
可以让核心线程在空闲时间超过keepAliveTime
后被回收,适用于负载波动较大的场景。 - 保活机制设置:通过设置
keepAliveTime
来控制空闲线程的保活时间,再结合allowCoreThreadTimeOut
控制核心线程的保活行为。