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

本文主要是介绍Springboot中开启多线程,实现异步非阻塞、异步阻塞、有无返回值的场景,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

需求背景

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

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

这篇关于Springboot中开启多线程,实现异步非阻塞、异步阻塞、有无返回值的场景的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/274223

相关文章

C#借助Spire.XLS for .NET实现在Excel中添加文档属性

《C#借助Spire.XLSfor.NET实现在Excel中添加文档属性》在日常的数据处理和项目管理中,Excel文档扮演着举足轻重的角色,本文将深入探讨如何在C#中借助强大的第三方库Spire.... 目录为什么需要程序化添加Excel文档属性使用Spire.XLS for .NET库实现文档属性管理Sp

Python+FFmpeg实现视频自动化处理的完整指南

《Python+FFmpeg实现视频自动化处理的完整指南》本文总结了一套在Python中使用subprocess.run调用FFmpeg进行视频自动化处理的解决方案,涵盖了跨平台硬件加速、中间素材处理... 目录一、 跨平台硬件加速:统一接口设计1. 核心映射逻辑2. python 实现代码二、 中间素材处

Java方法重载与重写之同名方法的双面魔法(最新整理)

《Java方法重载与重写之同名方法的双面魔法(最新整理)》文章介绍了Java中的方法重载Overloading和方法重写Overriding的区别联系,方法重载是指在同一个类中,允许存在多个方法名相同... 目录Java方法重载与重写:同名方法的双面魔法方法重载(Overloading):同门师兄弟的不同绝

Spring配置扩展之JavaConfig的使用小结

《Spring配置扩展之JavaConfig的使用小结》JavaConfig是Spring框架中基于纯Java代码的配置方式,用于替代传统的XML配置,通过注解(如@Bean)定义Spring容器的组... 目录JavaConfig 的概念什么是JavaConfig?为什么使用 JavaConfig?Jav

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Java中ArrayList与顺序表示例详解

《Java中ArrayList与顺序表示例详解》顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,:本文主要介绍Java中ArrayList与... 目录前言一、Java集合框架核心接口与分类ArrayList二、顺序表数据结构中的顺序表三、常用代码手动

JAVA项目swing转javafx语法规则以及示例代码

《JAVA项目swing转javafx语法规则以及示例代码》:本文主要介绍JAVA项目swing转javafx语法规则以及示例代码的相关资料,文中详细讲解了主类继承、窗口创建、布局管理、控件替换、... 目录最常用的“一行换一行”速查表(直接全局替换)实际转换示例(JFramejs → JavaFX)迁移建

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

Python实现快速扫描目标主机的开放端口和服务

《Python实现快速扫描目标主机的开放端口和服务》这篇文章主要为大家详细介绍了如何使用Python编写一个功能强大的端口扫描器脚本,实现快速扫描目标主机的开放端口和服务,感兴趣的小伙伴可以了解下... 目录功能介绍场景应用1. 网络安全审计2. 系统管理维护3. 网络故障排查4. 合规性检查报错处理1.

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程