概念
线程池(Thread Pool)是一种基于池化技术的多线程处理形式,用于管理线程的创建和生命周期,以及提供一个用于并行执行任务的线程队列。线程池的主要目的是减少在创建和销毁线程时所花费的开销和资源,提高程序性能,同时也提供了对并发执行任务的更好管理,例如控制线程数量。
使用线程池的好处
- 线程复用:线程池中的线程可以被重复利用,用于执行多个任务,避免了频繁创建和销毁线程的性能开销。提高响应速度
- 资源控制:线程池可以限制系统中线程的最大数量,防止因为线程数过多而消耗过多内存,或者导致过高的上下文切换开销。
- 更方便的管理:通过线程池提供了可配置的参数,如核心线程数、最大线程数、空闲线程存活时间、任务队列的大小等,允许定制以适应不同的应用需求
线程池七大参数详解
核心线程数
概念
这是线程池中始终保持的线程数量,即使它们处于空闲状态也不会被回收,这样保证当新任务到来时能够快速进行响应,核心线程数设置的太小会导致用户请求响应不及时,核心线程数设置的太大会导致线程资源浪费,下文会讲解如何设计核心线程数
任务队列
概念
线程池中的任务队列是一个非常重要的组件,它用于存储等待被线程池中的线程执行的任务,我们上文中提到了,核心线程数不宜设置过多,否则会造成线程资源浪费和cpu上下文频繁切换的问题,核心线程能够满足系统平均流量即可,所以当某些特殊情况下系统流量激增的时候
当所有核心线程都忙于处理任务时,那么新任务将被放入任务队列中排队等待
评估任务队列的大小
任务队列的大小,可以设计为核心线程每秒所能处理的任务数的 2倍即可,比如我们上面案例中计算出了核心线程数为4,每秒可以处理20个请求,那么任务队列的大小设置为40即可
任务队列类型
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());
用它来做任务队列几乎不会被填满,因此通常不会触发拒绝策略,线程池的非核心线程(即大于核心线程数的线程)只有在任务队列已满的情况下才会被创建。由于任务队列几乎不会被填满,因此非核心线程几乎不会被创建。即无法创建非核心线程去处理任务,也无法抛出拒绝策略,如果任务提交速率高于任务处理速率,会有越来越多的任务对象堆积在这个阻塞队列中,造成较大的内存压力甚至导致OOM,此外,如果是我们上线或者重启服务,阻塞队列中任务会丢失,需要采取额外策略去解决阻塞队列任务丢失风险
如果我的线程池任务都是不可丢弃的,且我不想使用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
这个策略将尝试丢弃队列中最早的一个任务,然后尝试再次提交新任务(不保证成功)。这种方式在任务可以被丢弃且希望运行最新任务时可能会有用。
如何设计线程池核心线程数目与最大线程数?
首先评估当前需要使用线程池的业务是属于cpu密集型还是io密集型
- 如果cpu密集型,那么线程的数量不宜设置过多,因为如果线程数量过多,线程数最好不要超过cpu核心数,如果线程数远大于cpu核心数的话,那么会伴随大量的cpu上下文切换,浪费cpu时间,此外,多线程开发中我们经常会对一些共享资源加锁,线程数目过多会导致锁竞争加剧。
- 如果是io密集型则相反,线程的数量需要多设置一些,线程数最好是cpu核心数的2倍以上,原因是io密集型环境中一个线程执行过程中往往伴随大量的io阻塞,我们所希望的是,当一个线程进入io阻塞状态时,它所持有的cpu资源被释放出来后会立马被其他线程抢占,这样可以确保cpu资源被最大化利用,所以这一切的前提就是要把线程数量设置的多一些,否则如果线程数量不够多的话,这些为数不多的线程进入阻塞后,cpu资源大部分都是空闲状态,很多用户请求阻塞在线程池排队队列中,但受限于线程数不够无法充分利用这些cpu资源,导致线程池用户请求排队队列中的请求越积越多
评估系统的qps
案例一:cpu密集型业务场景,我们主机的cpu核心数是24,系统稳定情况下平均流量为20QPS,每个请求执行时间为0.2秒,那么核心线程数应该设置为多少?
每个请求的执行时间为0.2秒,平均流量为20 QPS,那么每秒需要的计算资源是 20×0.2= 4个核心。所以我们的核心线程数设置为4个即可
案例二:cpu密集型业务场景,我们主机的cpu核心数是24,系统稳定情况下平均流量为200QPS,每个请求执行时间为0.2秒,那么核心线程数应该设置为多少?
每个请求的执行时间为0.2秒,平均流量为200 QPS,那么每秒需要的计算资源是 200×0.2= 40个核心。但我们的主机cpu核心数只有24,也就是说我们cpu的处理能力最大是每秒处理120个任务,也就是每秒都会堆积80个无法被处理的任务,除非是采用直接抛弃多余任务的拒绝策略,否则任务对象的数目越积越多可能会导致OOM(下文中会详细介绍这种情况),这已经不是增加线程数可以应对的了,而是要考虑给当前的服务集群加主机去分担流量了,比如再加一台cpu核心数为24的服务器
如何保证重启服务时,线程池的阻塞队列中排队中的任务不会丢失?
线程池的shutdown方法
调用 线程池的shutdown 方法,会优雅的关闭线程池,所谓的优雅就是不会立马粗暴的关闭线程池,虽然它不再接受新的任务,但会继续执行已经提交的任务,包括在队列中的任务
shutdownNow()方法用于立即关闭线程池。调用该方法后,线程池将尝试停止所有正在执行的任务,并返回等待执行的任务列表
@Slf4j
public class ThreadPoolShutdownExample {
public static final ExecutorService CAMPAIGN_EXECUTOR_POOL =
new ThreadPoolExecutor(
2,
2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new DefaultThreadFactory("test"));
public static void main(String[] args) {
// 提交一些任务,这些任务一部分会在执行,另一部分则是会存入排队队列中
for (int i = 0; i < 20; i++) {
final int taskId = i;
CAMPAIGN_EXECUTOR_POOL.submit(() -> {
try {
System.out.println("Executing task " + taskId);
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Task " + taskId + " was interrupted.");
}
});
}
// 调用 shutdown 方法,优雅的关闭线程池
CAMPAIGN_EXECUTOR_POOL.shutdown();
System.out.println("Shutdown initiated...");
// 此时新来的任务线程池不会再接受了,并且抛出异常
try {
for (int i = 20; i < 40; i++) {
final int taskId = i;
CAMPAIGN_EXECUTOR_POOL.submit(() -> {
try {
System.out.println("Executing task " + taskId);
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Task " + taskId + " was interrupted.");
}
});
}
} catch (Exception e) {
log.error("ThreadPoolShutdownExample catch exception:{}", e);
}
// 此处等待所有任务完成,超时设置为 5 秒,因为调用了shutdown方法需要等线程池所有剩余任务都被处理完才能顺利关闭线程,
// 如果线程池中的排队任务较多5秒内无法处理完剩余任务,那么就调用shutdownNow,立刻强制关闭线程池,线程池剩余排队任务也丢弃掉
try {
if (!CAMPAIGN_EXECUTOR_POOL.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("Forcing shutdown...");
CAMPAIGN_EXECUTOR_POOL.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
System.err.println("Termination interrupted");
CAMPAIGN_EXECUTOR_POOL.shutdownNow(); // 发生异常时强制关闭
}
System.out.println("Main thread exiting.");
}
}
模拟线上环境解决方案
思路非常简单,使用一个ThreadPoolUtils统一管理我们系统所需要的全部线程池,使用@PreDestroy定义一个springboot服务关闭前自动调用的方法
@Component
public class ThreadPoolUtils {
public static final ExecutorService CAMPAIGN_EXECUTOR_POOL =
new ThreadPoolExecutor(
8,
8,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new DefaultThreadFactory("CAMPAIGN_EXECUTOR_POOL"));
public static final ExecutorService FB_ASYNC_EXECUTOR = new ThreadPoolExecutor(
2,
10,
0L,
TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
new DefaultThreadFactory("FB_ASYNC_EXECUTOR"),
new ThreadPoolExecutor.CallerRunsPolicy());
public static final ExecutorService GG_ASYNC_EXECUTOR = new ThreadPoolExecutor(
2,
10,
0L,
TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
new DefaultThreadFactory("GG_ASYNC_EXECUTOR"),
new ThreadPoolExecutor.CallerRunsPolicy());
public static final ExecutorService MPA_ASYNC_SHARED_AUDIENCE_EXECUTOR = new ThreadPoolExecutor(
4,
10,
0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(300),
new DefaultThreadFactory("MPA_ASYNC_SHARED_AUDIENCE_EXECUTOR"),
new ThreadPoolExecutor.AbortPolicy());
@PreDestroy
public void shutdownExecutor() throws NoSuchMethodException {
System.out.println("Shutting down the executor service...");
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
if (ExecutorService.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
try {
ExecutorService executorService = (ExecutorService) field.get(this);
executorService.shutdown(); // Shutdown the executor service gracefully
try {
// 等待直到所有已经提交的任务完成,超时设置为 5 秒
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("Forcing shutdown...");
executorService.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
System.err.println("Shutdown interrupted");
executorService.shutdownNow(); // 发生异常时强制关闭
Thread.currentThread().interrupt();
}
System.out.println("Executor service shutdown complete.");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
线程池的应用
框架中的应用
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);
}
}
延时异步任务
我需要开启一个异步任务,但这个异步任务需要等待30秒后再执行,最简单粗暴的方法是使用Thread.sleep方法,但这种的话会造成线程资源的浪费,高并发情况下就容易出现线程资源紧缺的问题,所以如何高效率实现这一需求呢?推荐使用ScheduledExecutorService,可以更加优雅地处理延迟任务。在指定的延迟时间后执行任务,而不会在此期间占用线程资源。
public class AsyncTaskExample {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 提交一个30秒后执行的任务
executorService.schedule(() -> {
// 这里编写任务完成后要执行的代码
System.out.println("30秒已过,异步任务完成。");
}, 30, TimeUnit.SECONDS);
// 继续执行主线程的其他任务
System.out.println("主线程继续执行...");
// 关闭执行器服务 (根据需要选择适当的位置关闭)
executorService.shutdown();
}
}