需求背景
近期项目已上线,闲着没事就对功能进行性能测试,测着测着感觉部分功能效果不是很理想,于是就想着使用多线程的方式对部分接口进行优化,顺便在这里记录下如何选择使用多线程。
实现多线程有两种开启方式:分别是使用xml文件配置和注解的方式,想要简单方便的肯定优先使用注解啊,在Springboot中使用注解开启多线程主要包含以下步骤:
1、项目启动类上添加@EnableAsync注解,表示开启支持异步任务;
2、创建配置线程池,使用@Configuration和@Bean注解交由Spring容器管理;
3、使用@Async注解标记异步任务;
基本概念
步骤已经清楚了,接下来我们先来大概了解下概念:
1、同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去;(举个例子:当你去上厕所时只有一个卫生间,恰好卫生间有人正在使用,这个时候你必须要等待上个人使用完毕);其实这个概念也可以称为阻塞状态。
2、 异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据;(举个例子:当你去上厕所时有多个卫生间,部分卫生间被占用,但是可以使用别的卫生间,不需要等待别人,甚至还能边上边来一根)。这个概念也可以称为非阻塞状态。
代码实现
基本步骤和概念都清楚了,那就开始上代码,根据不同的场景需求来编写不同的多线程任务。
场景一(异步非阻塞且无返回值)
1、启动类添加 @EnableAsync 注解;
2、创建配置线程池(可复制粘贴,基本通用);
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @Author: ljh
* @ClassName AsynConfig
* @Description TODO
* @date 2023/10/21 11:03
* @Version 1.0
*/
@Configuration
public class AsyncConfig {
@Bean("asyncconfig")
public Executor doSomethingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池创建时候初始化的线程数
executor.setCorePoolSize(10);
// 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(20);
// 缓冲队列:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
// 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("async-");
// 缓冲队列满了之后的拒绝策略:由调用线程处理(一般是主线程)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
3、编写异步任务,使用 @Async 注解进行标记;
@Async("asyncconfig")
@Override
public void asyncText(Integer num) {
System.err.println(num);
}
4、调用异步任务进行测试,注意:调用方法和被调用任务不可以放在同一个类中,否则会导致@Async失效,我是分别放在了Controller和ServiceImpl层;
@ApiOperation("测试异步任务")
@PostMapping("/asyncText")
public void asyncText() {
System.err.println("==========主线程开始==========");
for(int i = 0; i < 10; i++){
System.err.println("----------子线程开始----------");
//调用ServiceImpl层编写的异步任务
baseInfoService.asyncText(i);
System.err.println("----------子线程结束----------");
}
System.err.println("==========主线程结束==========");
}
打印结果:
==========主线程开始==========
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
0
1
----------子线程开始----------
2
----------子线程结束----------
----------子线程开始----------
3
----------子线程结束----------
----------子线程开始----------
4
----------子线程结束----------
----------子线程开始----------
5
----------子线程结束----------
----------子线程开始----------
6
7
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
==========主线程结束==========
8
9
由以上打印结果进行分析:可以看到在主线程结束后依然打印了8、9,这说明主线程和子线程是异步的,主线程是不需要等待子线程是否全部执行完毕,这就是异步非阻塞的形式。
场景二(异步非阻塞且有返回值)
异步非阻塞且有返回值的场景其实是不存在的,为什么这样说呢?因为想要获取子线程的返回值,是不是必须要等待子线程执行完毕,如果不等待子线程执行完毕那么获取到的值只能是null,只有等待子线程执行完毕才能获取到想要的值,要等待只能是阻塞,所以异步非阻塞且有返回值的场景几乎是不存在的,除非你子线程有返回值但是结果又对你来说不重要没影响,这样的话还要返回值干什么呢。
场景三(异步阻塞且无返回值)
1、2步骤与前面一致,这里不在赘述;
3、编写异步任务,使用 @Async 注解进行标记;
@Async("asyncconfig")
@Override
public void asyncText(Integer num,CountDownLatch latch) {
System.err.println(num);
latch.countDown();
}
4、调用异步任务进行测试,注意:调用方法和被调用任务不可以放在同一个类中,否则会导致@Async失效,我是分别放在了Controller和ServiceImpl层;
@ApiOperation("测试同步异步任务")
@PostMapping("/asyncText")
public void asyncText() {
System.err.println("==========主线程开始==========");
CountDownLatch latch = new CountDownLatch(10);
for(int i = 0; i < 10; i++){
System.err.println("----------子线程开始----------");
baseInfoService.asyncText(i,latch);
System.err.println("----------子线程结束----------");
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("==========主线程结束==========");
}
打印结果
==========主线程开始==========
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
1
0
----------子线程结束----------
----------子线程开始----------
3
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
2
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
4
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
5
6
7
8
9
==========主线程结束==========
由以上打印结果进行分析:在这里可以看到,主线程任务是等待全部子线程执行完毕后才执行结束的,也就是在执行异步子线程时阻塞当前主线程必须等待子线程全部执行完毕后才能继续执行主线程,实现方式就是使用了 CountDownLatch 类应用计数器的原理,使用CountDownLatch时需要先定义计数器的大小,因为我这里是写的循环,所以计数器大小就是循环的次数,异步任务中的countDown() 方法是每次计数器进行减一,await() 方法则是阻塞当前线程,然后等待计数器为0时才会被唤醒当前线程继续执行。
场景四(异步阻塞且有返回值)
1、2步骤与前面一致,这里不在赘述;
3、编写异步任务,使用 @Async 注解进行标记;
@Async("asyncconfig")
@Override
public CompletableFuture<String> asyncText(Integer num) {
return CompletableFuture.completedFuture(String.valueOf(num));
}
4、调用异步任务进行测试,注意:调用方法和被调用任务不可以放在同一个类中,否则会导致@Async失效,我是分别放在了Controller和ServiceImpl层;
@ApiOperation("测试同步异步任务")
@PostMapping("/asyncText")
public void asyncText() {
System.err.println("==========主线程开始==========");
List<CompletableFuture<String>> list = new ArrayList<>();
for(int i = 0; i < 10; i++){
System.err.println("----------子线程开始----------");
CompletableFuture<String> future = baseInfoService.asyncText(i);
list.add(future);
System.err.println("----------子线程结束----------");
}
for(CompletableFuture<String> str : list){
try {
//阻塞,直至 str的异步线程执行完毕
CompletableFuture.allOf(str).join();
System.err.println(str.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
System.err.println("==========主线程结束==========");
}
执行结果:
==========主线程开始==========
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
----------子线程开始----------
----------子线程结束----------
0
1
2
3
4
5
6
7
8
9
==========主线程结束==========
由以上打印结果进行分析:在这里呢主要是使用到了 CompletableFuture ,首先需要定义异步任务的返回值类型为 CompletableFuture< String > 或 CompletableFuture< Integer> 或其它需要的类型,调用异步任务后需要先将结果存起来,为什么不直接获取结果而是存起来呢,因为任务是异步的,如果子线程没有执行完毕获取的结果只是null,所以结果集存放起来后呢使用 CompletableFuture.allOf(str).join() 方式阻塞主线程必须等待子线程执行完毕,然后才能使用 get() 方法来获取最终的结果。
总结
开启多线程异步的方式有很多种,不单单局限以上方式,感兴趣的可以自行研究测试下,比如还可以使用 ThreadPoolTaskExecutor 来开启多线程,然后分别使用对应的 execute() 和 submit() 方法实现无返回值和有返回值的效果;以上内容均为个人理解,如存在不当欢迎提出改进。