概念
线程池(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());
适用场景
- 适用于需要快速处理的高并发短任务。对实时性要求高的任务处理场景。比如支付场景、实时聊天场景
- 适用于无核心线程的场景,比如实时监控报警系统,监控系统需要对异常事件进行实时处理和报警。当事件发生时,系统需要迅速处理并生成报警信息,但在没有事件发生时,不需要占用过多的系统资源。
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());
由于 LinkedBlockingQueue 的容量是 Integer.MAX_VALUE,任务队列几乎不会被填满,因此通常不会触发拒绝策略,线程池的非核心线程(即大于核心线程数的线程)只有在任务队列已满的情况下才会被创建。由于任务队列几乎不会被填满,因此非核心线程几乎不会被创建。即无法创建非核心线程去处理任务,也无法抛出拒绝策略,如果任务提交速率高于任务处理速率,任务将会堆积在内存中,对系统造成较大的内存压力,此外,如果是我们上线或者重启服务,阻塞队列中任务会丢失,需要采取额外策略去解决阻塞队列任务丢失风险
如果我的线程池任务都是不可丢弃的,且我不想使用callerRunPolicy,因为我的线程任务是纯异步的,我不想让把线程任务抛给主线程影响我的主业务流程,这种情况下我可以适当把阻塞队列设置的大一些,这种方法虽然可以在一定程度上缓解拒绝策略被触发的问题,但并不是一个根本解决方案,特别是在任务提交速率持续高于处理速率的情况下,归根到底想要解决这个问题还是要加资源(提高核心线程池与最大线程数)
最大线程数
这是线程池可以同时运行的最大线程数量。当工作队列满了,且当前运行的线程数少于最大线程数时,线程池会创建新的线程来处理任务。需要注意,最大线程数中是包括核心线程数的,最大线程数的比如遇到双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请求时,每个请求都可以作为一个独立的任务提交到线程池中,由线程池中的线程处理,这样做的好处是可以快速响应用户请求,同时复用线程资源
开发中的常见常见
异步任务处理
例如发送电子邮件、执行后台计算等这些都可以作为异步任务提交给线程池,从而不会阻塞主程序的执行。
并行数据处理
比如我从mysql中去取出10万条数据,结果集是一个List,分批次处理,每个批次1000条,在for循环中循环的次数就是批次的数目,每次循环会提交给线程池一个futureTask异步运算任务,比如这里会分为100个批次,那么会for循环100次提交100个futureTask异步运算任务,线程池中的线程会并行去处理这些批次的数据,然后再把每个处理后的批次组合为一个最终结果
public class BatchDataProcessor {
// 每批次处理的数据量
private static int BATCH_SIZE = 5;
// 创建一个固定大小的线程池
private ExecutorService executorService = Executors.newFixedThreadPool(10);
public void process(List<String> allDataList) {
if (CollectionUtils.isEmpty(allDataList)) {
return;
}
List<List<String>> batchDatas = Lists.partition(allDataList, BATCH_SIZE);
// 用于存储所有的CompletableFuture
List<CompletableFuture<List<String>>> futureList = new ArrayList<>();
for (List<String> batchData : batchDatas) {
// 分别为每个批次的数据创建一个CompletableFuture任务
CompletableFuture<List<String>> future =
CompletableFuture.supplyAsync(() -> processData(batchData), executorService);
// 将FutureTask添加到任务列表中
futureList.add(future);
}
// 使用allOf方法,将所有的CompletableFuture组合为一个
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
// 当所有的CompletableFuture都完成时,获取并处理所有批次的结果
CompletableFuture<List<String>> allResultsFuture = allFutures.thenApply(v -> futureList.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.collect(Collectors.toList()));
try {
// 获取并处理所有批次的结果
List<String> result = allResultsFuture.get();
for (String s : result) {
System.out.println(s);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
executorService.shutdown();
}
// 处理数据的逻辑
private List<String> processData(List<String> batchData) {
if (CollectionUtils.isEmpty(batchData)) {
return batchData;
}
int index = 0;
for (String batchDatum : batchData) {
batchData.set(index, batchDatum + "had process");
index++;
// System.out.println(batchDatum + " " +Thread.currentThread().getName() + " had process");
}
// 模拟其它业务耗时
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return batchData;
}
public static void main(String[] args) {
BatchDataProcessor batchDataProcessor = new BatchDataProcessor();
// 造数据
List<String> allDataList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
allDataList.add("data" + i);
}
batchDataProcessor.process(allDataList);
}
}