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

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法