Springboot中开启多线程,实现异步非阻塞、异步阻塞、有无返回值的场景

48 篇文章 4 订阅
3 篇文章 0 订阅

需求背景

近期项目已上线,闲着没事就对功能进行性能测试,测着测着感觉部分功能效果不是很理想,于是就想着使用多线程的方式对部分接口进行优化,顺便在这里记录下如何选择使用多线程。

实现多线程有两种开启方式:分别是使用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() 方法实现无返回值和有返回值的效果;以上内容均为个人理解,如存在不当欢迎提出改进。

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在Java,如果希望在不影响主线程运行的情况下处理异步方法的返回值,则可以使用以下方法之一: 1. 使用`Executor`和`Future`:可以使用`Executor`框架的`ExecutorService`来创建一个线程池,然后使用`submit`方法提交一个异步任务,该方法将返回一个`Future`对象,可以使用`get`方法获取异步任务的返回值。例如: ``` ExecutorService executor = Executors.newFixedThreadPool(10); Future<Integer> result = executor.submit(new Callable<Integer>() { public Integer call() throws Exception { // 异步处理的代码 return 123; } }); // 在主线程处理异步方法的返回值 int value = result.get(); ``` 2. 使用`CompletableFuture`:Java 8引入了`CompletableFuture`类,它是对`Future`的增强版,支持将多个异步任务串联起来,并提供了更丰富的回调机制。例如: ``` CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { // 异步处理的代码 return 123; }); // 在主线程处理异步方法的返回值 int value = future.get(); ``` 3. 使用`CompletionStage`:Java 9引入了`CompletionStage`接口,它也支持将多个异步任务串联起来,但是和`CompletableFuture`相比,它更加灵活,可以使用不同的实现类来实现异步流程。例如: ``` CompletionStage<Integer> stage = CompletableFuture.supply ### 回答2: 在Java,要处理异步方法的返回值而不影响主线程的运行流程,可以使用线程池和Future对象来实现。 首先,创建一个线程池来管理线程的执行。可以使用Executors类的静态工厂方法创建一个线程池,例如: ``` ExecutorService executor = Executors.newFixedThreadPool(1); ``` 然后,将异步方法的调用封装在Callable对象,使用submit方法将Callable对象提交给线程池执行,并返回一个Future对象,例如: ``` Callable<String> asyncMethod = () -> { // 异步方法的逻辑 return "异步方法的返回值"; }; Future<String> future = executor.submit(asyncMethod); ``` 接下来,在主线程可以继续执行其他任务,而不用等待异步方法的返回结果。如果需要获取异步方法的返回值,可以使用Future对象的get方法,该方法会阻塞主线程直到异步方法执行完成并返回结果,例如: ``` String result = future.get(); ``` 需要注意的是,get方法也可以设置超时时间,当异步方法执行时间超过超时时间时,get方法会抛出TimeoutException异常。 最后,在不需要线程池时,可以使用shutdown方法关闭线程池,例如: ``` executor.shutdown(); ``` 通过使用线程池和Future对象,可以实现在Java处理异步方法的返回值而不影响主线程的运行流程。 ### 回答3: 在Java,要处理异步方法的返回值而不影响主线程运行的流程,可以使用Future和CompletableFuture机制。 Future是Java标准库提供的异步编程机制之一,通过Future实例可以获取异步任务的返回结果。在主线程,可以使用Future.get()方法来等待异步任务的完成,并获取异步任务的返回结果。这样,主线程会在此处阻塞,直到异步任务完成。 但是,如果主线程需要继续执行其他任务,而不是阻塞等待异步任务的完成,可以使用CompletableFuture。CompletableFuture是在Java 8引入的,它提供了更多便利的方法用于异步编程。通过CompletableFuture,我们可以更加灵活地处理需要异步执行的任务。 使用CompletableFuture,我们可以通过thenApply()、thenAccept()、thenRun()等方法将异步任务与主线程的后续操作串联起来。这样,异步任务的结果会在主线程的某个位置被处理,而主线程可以继续执行其他任务。同时,我们还可以通过调用CompletableFuture.join()方法,使主线程等待异步任务的完成。 总结起来,Java可以通过Future和CompletableFuture来处理异步方法的返回值,而不影响主线程的运行流程。使用Future时,主线程会在调用get()方法处阻塞等待,而使用CompletableFuture时,主线程可以继续执行其他任务,并在合适的位置处理异步任务的返回结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想养一只!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值