概念
线程池(Thread Pool)是一种基于池化技术的多线程处理形式,用于管理线程的创建和生命周期,以及提供一个用于并行执行任务的线程队列。线程池的主要目的是减少在创建和销毁线程时所花费的开销和资源,提高程序性能,同时也提供了对并发执行任务的更好管理,例如控制线程数量。
使用线程池的好处
- 线程复用:线程池中的线程可以被重复利用,用于执行多个任务,避免了频繁创建和销毁线程的性能开销。提高响应速度
- 资源控制:线程池可以限制系统中线程的最大数量,防止因为线程数过多而消耗过多内存,或者导致过高的上下文切换开销。
- 更方便的管理:通过线程池提供了可配置的参数,如核心线程数、最大线程数、空闲线程存活时间、任务队列的大小等,允许定制以适应不同的应用需求
线程池七大参数详解
核心线程数
这是线程池中始终保持的线程数量,即使它们处于空闲状态也不会被回收,这样保证当新任务到来时能够快速进行响应
核心线程数的计算要参考系统中占比大部分时间的稳定流量来计算,因为核心线程数如果设置的过大可能会造成线程资源的浪费,比如系统的流量平均为100QPS,每个请求执行时间为0.2秒,那么核心线程数设置为100 * 0.2 = 20个
而针对请求高峰期的特殊情况,我们可能通过设置最大线程数来应对,这使得线程资源能够被最高兴的利用
任务队列
线程池中的任务队列是一个非常重要的组件,它用于存储等待被线程池中的线程执行的任务,当所有核心线程都忙于处理任务,那么新任务将被放入任务队列中排队等待,只有当任务队列已满时,线程池才会尝试创建新的非核心线程来处理任务,直到线程池中的线程数量达到最大线程数
任务队列的大小,可以设计为核心线程每秒所能处理的任务数的 2倍即可,比如我们上面计算出了核心线程数为20,每秒可以处理100个请求,那么任务队列的大小设置为200即可
常见任务队列
SynchronousQueue
它与其他阻塞队列不同的地方在于,它内部并不持有任何数据元素,也就是说它没有任何内部容量(容量为0)。SynchronousQueue 的工作方式更像是一个传递手棒的机制,它适用于将任务直接从生产者传递给消费者的场景。
例如我们可以这么定义线程池
private final ExecutorService executor = new ThreadPoolExecutor(0, 200,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
比如我系统中有一个定时任务,执行时间是每晚凌晨1点到3点,这个时候我核心线程设为0,阻塞队列为0,最大线程设为200,,这样的好处是
- 核心线程数为0,意味着定制任务非执行时间不会占用任何线程资源,避免浪费
- 定时任务开始执行后,每来一个任务就会创建一个线程,可以快速创建线程响应请求,且线程的最大空闲时间被设置为60秒,意味着在定时任务执行期间这些非核心线程也不会被频繁的创建销毁
LinkedBlockingQueue
LinkedBlockingQueue是有容量的,可以用作阻塞队列,默认容量是Integer.MAX_VALUE,可看做是无穷大的容量,这意味着无论多大的并发量都会去排队执行,不会触发拒绝策略
private static final ExecutorService executor = new ThreadPoolExecutor(32,
32, 60000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new CommonThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
也可以通过构造器参数指定队列容量
最大线程数
这是线程池可以同时运行的最大线程数量。当工作队列满了,且当前运行的线程数少于最大线程数时,线程池会创建新的线程来处理任务。需要注意,最大线程数中是包括核心线程数的,最大线程数的比如遇到双11、618这种请求高峰期,可能QPS会达到500,根据这个计算得出,最大线程数的计算公式 = (系统最大请求任务数 - 任务队列任务数)
拒绝策略
AbortPolicy
这个策略会直接抛出一个RejectedExecutionException异常,从而拒绝新任务的执行。这是默认的策略,这种策略也算是丢弃了任务任务,只不过相比DiscardPolicy,AbortPolicy通过抛出异常显示的拒绝任务,这样在任务调用的地方,开发者可以通过异常捕获来做日志打印、利用递归重新提交任务等操作,但需要注意,高并发情况下可能同一个任务再重试的时候会频繁触发拒绝策略导致不断重试,这样的话会造成严重性能损耗,所以采取两个策略①等待重试,即让线程sleep一段时间再进行重试②限制重试次数,案例代码如下
public class RetryTaskExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
10, // 空闲线程的存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(2), // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 1; i <= 6; i++) {
final int taskId = i;
submitTask(executor, taskId, 0);
}
// 关闭线程池
executor.shutdown();
}
private static void submitTask(ThreadPoolExecutor executor, int taskId, int count) {
try {
executor.execute(() -> {
System.out.println("Running task " + taskId + " by " + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Task " + taskId + " was interrupted.");
}
});
System.out.println("Successfully submitted task " + taskId);
} catch (RejectedExecutionException e) {
// 日志记录
System.out.println("Task " + taskId + " rejected: " + e.getMessage());
System.out.println("Trying to resubmit task " + taskId);
// 简单重试策略
try {
if (count < 3) {
// 等待一段时间后重试
TimeUnit.MILLISECONDS.sleep(500);
submitTask(executor, taskId, ++count); // 递归调用,重新提交任务
}
} catch (InterruptedException ex) {
System.out.println("Retry of task " + taskId + " was interrupted.");
}
}
}
}
DiscardPolicy
相比于这个策略将悄无声息地丢弃无法处理的任务,不会抛出异常,也不会给调用者任何提示。
CallerRunsPolicy
将任务回退给调用者线程(即提交任务的线程)来直接执行,这种方式会降低对线程池的新任务提交速度,因为提交任务的线程被占用来执行任务,但可以保证无论何种情况任务都会被提交并执行,虽然不是默认的策略,但确实实际开发中用的最多的策略。案例代码
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
10, // 空闲线程的存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(2), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
Long startTime = System.currentTimeMillis();
// 通过for循环来模拟高并发的场景
// 主线程通过调用方法submitTask来提交任务,线程池中的线程来执行任务,但在并发量过大,超出了线程池的
for (int i = 1; i <= 10; i++) {
final int taskId = i;
submitTask(executor, taskId, 0);
}
// 关闭线程池
executor.shutdown();
}
日志打印为
由此可知,第1,2个任务由核心线程去执行,第3,4个线程存储在任务队列中,第5,6个线程由非核心线程去处理,第7,8个线程就会由main线程去处理,只有等main线程执行完毕后才会去提交并执行剩余的任务
DiscardOldestPolicy
这个策略将尝试丢弃队列中最早的一个任务,然后尝试再次提交新任务(不保证成功)。这种方式在任务可以被丢弃且希望运行最新任务时可能会有用。
线程池的应用
框架中的应用
Web服务器
在处理HTTP请求时,每个请求都可以作为一个独立的任务提交到线程池中,由线程池中的线程处理,这样做的好处是可以快速响应用户请求,同时复用线程资源
开发中的常见常见
异步任务处理
例如发送电子邮件、执行后台计算等这些都可以作为异步任务提交给线程池,从而不会阻塞主程序的执行。
并行数据处理
在处理大数据集时,可以将数据分割成小块,并行地在多个线程上进行处理,例如