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

news2024/12/23 20:12:10

需求背景

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1126263.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习笔记-MongoDB(命令增删改查,聚合,权限管理,索引,java使用)

基础概念 1 什么是mogodb&#xff1f; MongoDB 是一个基于分布式文件/文档存储的数据库&#xff0c;由 C 编写&#xff0c;可以为 Web 应用提供可扩展、高性能、易部署的数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库中功…

Linux备份Docker的mysql数据并传输到其他服务器保证数据级容灾

目录 简介什么是容灾 &#xff1f;容灾的分类容灾和备份有什么连系 &#xff1f; 数据级容灾备份步骤1、scp命令&#xff1a;用于Linux之间复制文件和目录2、编写备份数据库脚本3、crontab定时任务执行脚本4、测试 应用级容灾业务级容灾 简介 为了防止客户系统的数据丢失&…

2023Etsy入驻攻略——防封安全

2023了&#xff0c;跨境电商现在上车还来得及吗&#xff1f;当然&#xff01;Etsy是一个低成本低竞争高回报的平台&#xff0c;相较于其他电商平台&#xff0c;他的佣金非常低&#xff0c;利润率更高&#xff0c;非常合适跨境小白入局。 但由于目前Etsy关闭了中国大陆卖家的注…

论文解读:Large Language Models as Analogical Reasoners

一、动机 大模型在各种类型的NLP任务上均展现出惊艳的表现。基于CoT propmt能够更好地激发大模型解决复杂推理问题的能力&#xff0c;例如解决数学解题&#xff0c;可以让模型生成reasoning path。现有的经典的CoT方法有few-shot cot、zero-shot cot等。然后现有的cot面临两个…

C++进阶篇2---多态

1.多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是当不同的对象&#xff0c;去完成某个行为&#xff0c;会产生不同的状态 举个例子&#xff1a;同样是吃饭&#xff0c;狗吃狗粮&#xff0c;猫吃猫粮&#xff0c;不同的对象&#…

Javascript基础-DOM

文章目录 WEB APISDOM-Document Object Model概念DOM对象获取DOM对象通过css选择器获取其他获取方式 操作元素内容操作元素属性定时器 DOM-事件监听概念案例-关闭广告老版本事件类型事件对象获取事件对象属性 环境对象回调函数 事件流事件捕获事件冒泡阻止冒泡阻止默认行为 解绑…

【JAVA学习笔记】40 - 抽象类、模版设计模式(抽象类的使用)

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter10/src/com/yinhai/abstract_ 一、抽象类的引入 很多时候在创建类的时候有一个父类&#xff0c;比如animal类&#xff0c;他的子类会有各种方法&#xff0c;为了复用需要进行方法的重写&…

微信小程序OA会议系统个人中心授权登入

在我们的完成微信登入授权之前&#xff0c;首先我们要完成我们前面所写的代码&#xff0c;如果有不会的大家可以去看以下我发的前面几个文章链接我发下面了&#xff0c;各位加油&#xff01; 微信小程序OA会议系统数据交互-CSDN博客 微信小程序会议OA系统其他页面-CSDN博客 …

基于nodejs+vue视频网站的设计与实现mysql

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

机器学习---CNN(创建和训练一个卷积神经网络并评估其性能)上

1. cnn_operations模块 cnn_operations类 staticmethoddef calc_loss(Y, tilde_Y):# 训练样本个数n_samples Y.shape[0]# 网络代价loss 0for i in range(n_samples):loss np.sum((Y[i, :] - tilde_Y[i, :])**2)loss / (2 * n_samples)return loss计算网络代价&#xff1a; …

编程自学路线:开源免费的教育资源 | 开源专题 No.40

trekhleb/javascript-algorithms Stars: 174.1k License: MIT 这个项目是一个包含许多流行算法和数据结构的 JavaScript 示例。该项目提供了各种不同类型的数据结构&#xff0c;如链表、队列、栈等&#xff0c;并且还提供了各种常见的算法实现&#xff0c;如排序算法、搜索算…

Kubernetes技术与架构-网络 3

Kubernetes集群支持为Pod或者Service申请IPV4或者IPV6的地址空间。 kube-apiserver --service-cluster-ip-range<IPv4 CIDR>,<IPv6 CIDR> kube-controller-manager --cluster-cidr<IPv4 CIDR>,<IPv6 CIDR> --service-cluster-ip-range<IPv4 CI…

Java switch封神之路

Java switch升级之路 一&#xff0c;介绍 switch 是一种用于多分支条件判断的控制流语句。它通过检查一个表达式的值&#xff0c;然后根据不同的情况执行相应的代码块。 在大多数编程语言中&#xff0c;switch 语句由多个 case 分支组成&#xff0c;每个 case 后面跟着一个常…

unity游戏画质设置功能实现

在游戏中往往会出现游戏画质设置的功能。 如图&#xff1a; 这个功能是怎么实现完成的呢&#xff1f; 一、目标&#xff1a;实现切换画质功能 二、了解unity支持的画质 首先要了解unity中共支持多少种画质。 在代码中也可以进行打印。 方法如下&#xff1a; void Start …

NC61 两数之和

牛客网 NC61 两数之和 https://www.nowcoder.com/share/jump/7890810391698077140732 记录&#xff1a;维护哈希表&#xff0c;题目满足&#xff0c;numbers内必有两数相加为target&#xff0c;则可理解为&#xff0c;每次只需要判断target减去当前数&#xff0c;是否能在维护的…

zookeeper源码(02)源码编译启动及idea导入

本文介绍一下zookeeper-3.9.0源码下载、编译及本地启动。 下载源码 git clone https://gitee.com/apache/zookeeper.gitcd zookeeper git checkout release-3.9.0 git checkout -b release-3.9.0源码编译 README_packaging.md文件 该文件介绍了编译zookeeper需要的环境和命…

【JavaEE】UDP数据报套接字编程

一、UDP数据报套接字编程 1.1 DatagramSocket API DatagramSocket 是UDP Socket&#xff0c;用于发送和接收UDP数据报。 DatagramSocket 构造方法&#xff1a; DatagramSocket 方法&#xff1a; 1.2 DatagramPacket API DatagramPacket是UDP Socket发送和接收的数据报。…

NAS搭建指南三——私人云盘

一、私人云盘选择 我选择的是可道云进行私人云盘的搭建可道云官网地址可道云下载地址&#xff0c;下载服务器端和 Windows 客户端可道云官方文档 二、环境配置 PHP 与 MySQL 环境安装&#xff1a;XAMPP 官网地址 下载最新的 windows 版本 安装时只勾选 MySQL 与 PHP相关即可…

sklearn-6算法链与管道

思想类似于pipeline&#xff0c;将多个处理步骤连接起来。 看个例子&#xff0c;如果用MinMaxScaler和训练模型&#xff0c;需要反复执行fit和tranform方法&#xff0c;很繁琐&#xff0c;然后还要网格搜索&#xff0c;交叉验证 1 预处理进行参数选择 对于放缩的数据&#x…