Springboot异步多线程编程

article/2025/9/16 5:59:08

文章目录

  • 一、基础知识
  • 二、什么时候用同步&异步
  • 三、什么时候需要使用多线程
  • 四、springboot异步多线程编程实现

一、基础知识

同步:同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步:异步是指进程不需要一直等下去,而是继续执行下面的操作。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

进程:进程是独立的应用程序,占用cpu资源和物理内存。一个进程包括由操作系统分配的内存空间,包含一个或多个线程;
线程:线程是进程中虚拟的时间片,一个线程不能独立的存在,它必须是进程的一部分。
多线程:实际上就是时间片的轮转或者抢占。多线程能满足编写高效率的程序来达到充分利用CPU的目的;

二、什么时候用同步&异步

什么时候用同步:如果数据在线程间共享,例如正在写的数据可能被另外一个线程读到,而正在读的数据可能被另外一个线程写到,这些数据是共享的数据。这时就必须进行同步存取操作,否者前后读取的数据就有可能不一致;

什么时候用异步:调用一个需要花费很长时间来执行的方法的时候,并且不需要让程序等待对方返回,这时就应该使用异步编程;

必须使用同步的场景举例
有一个共享的银行账号,原来里面有余额1000元,现在有两个用户A,B都要进行取钱;
首先A查询账号剩余1000元,A想要取出200元,A点击取款,系统正在处理取款事项…
紧接着在A取款的过程中B查询同一个账号还有1000元,B也想要取走200元;
A取完款后剩余800元,正常。而B取完款后理论上应该剩余600元,但是实际上还是剩余800元。这种场景就必须使用同步,而不能使用异步;

三、什么时候需要使用多线程

举个例子
假设有个请求,服务端的处理需要执行3个比较耗时的操作:
1、操作1(200ms)
2、操作2(200ms)
3、操作3(200ms)
单线程总共就需要600ms,但如果把操作1、操作2、操作3分别分给3个线程去做,就只需要200ms了。

但是假设另外一个请求,服务端的处理也需要执行3个操作:
1、操作1(10ms)
2、操作2(10ms)
3、操作3(400ms)
单线程总共就需要420ms,这种情况下,即使把操作1、操作2、操作3分别分给3个线程去做,也需要400ms(耗时取决于最慢的那个线程的执行速度)。比起不用单线程,只节省了20ms。但是有可能线程调度切换也要花费个1、2ms。因此,这个方案显得优势就不明显了,还带来程序复杂度提升,不太值得,此时更好的方案是去优化降低操作3的耗时。

四、springboot异步多线程编程实现

4.1 使用idea创建springboot web项目,工程最终目录结构如下
在这里插入图片描述
4.2 首先创建springboot的线程池配置
common包下面创建ExecutorConfig类,用于自定义线程池的相关配置。使用@Configuration和@EnableAsync这两个注解,表示这是线程池的配置类。

@Configuration
@EnableAsync
@Slf4j
public class ExecutorConfig {/** 核心线程数(默认线程数) */private int corePoolSize = 10;/** 最大线程数 */private int maxPoolSize = 20;/** 允许线程空闲时间(单位:默认为秒) */private static final int keepAliveTime = 60;/** 缓冲队列大小 */private int queueCapacity = 10;@Beanpublic Executor asyncServiceExecutor(){log.info("start asyncServiceExecutor");ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();//配置核心线程数executor.setCorePoolSize(corePoolSize);//配置最大线程数executor.setMaxPoolSize(maxPoolSize);//配置空闲时间executor.setKeepAliveSeconds(keepAliveTime);//配置队列大小executor.setQueueCapacity(queueCapacity);//配置线程前缀名executor.setThreadNamePrefix("async-service-");// rejection-policy:当pool已经达到max size的时候,如何处理新任务// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//执行初始化executor.initialize();return executor;}
}

4.3 service层接口和实现
service包下新增server层的接口AsyncService类和对应的实现类AsyncServiceImpl。AsyncService内容如下:

public interface AsyncService {/*** 执行异步任务**/void executeAsync();
}

AsyncServiceImpl类内容如下,注意:
1.在executeAsync方法上增加注解@Async(“asyncServiceExecutor”) ;
2.@Async表示使用异步实现方式
3.括号里的asyncServiceExecutor是前面ExecutorConfig.java中的方法名,表明executeAsync方法使用asyncServiceExecutor方法创建的线程池多线程执行:

@Slf4j
@Service
public class AsyncServiceImpl implements AsyncService {//异步多线程调用@Async("asyncServiceExecutor")public void executeAsync() {log.info("start executeAsync");try{Thread.sleep(2000);}catch (Exception e){e.printStackTrace();}log.info("end executeAsync");}
}

4.4 controller层实现
创建HelloController类,新增/test http接口,调用server层的executeAsync服务。

@Slf4j
@RestController
public class HelloController {@Autowiredprivate AsyncService asyncService;//异步多线程调用方法,不用等方法返回结果@RequestMapping("/test")public String test(){log.info("start submit");//调用service层的任务asyncService.executeAsync();log.info("end submit");return "success";}
}    

4.5 验证效果

验证异步效果
1.先将ExecutorConfig类下corePoolSize设置为1,表示只用1个线程。然后运行springboot;
2.springboot启动成功后,在浏览器输入:http://localhost:8080/test 。 可以看到虽然我们前面AsyncServiceImpl代码中sleep了2秒,但由于使用的是异步实现,所以接口马上直接先返回了success,而不需要等待2秒后再返回。
在这里插入图片描述
后台日志也能看到,异步接口controller层很快就执行结束,然后service方法继续按代码执行了2秒:
在这里插入图片描述
验证多线程效果
1.corePoolSize设置为1时,使用Jmeter同时调用接口:http://localhost:8080/test 4次;
2.在springboot的控制台看见日志如下:
在这里插入图片描述
可以看出是1个线程每隔2秒执行完一次start&end executeAsync, 执行4次总共花费了8秒时间;

3.将corePoolSize设置为10,重启sprintboot;
4.再次使用Jmeter同时调用接口:http://localhost:8080/test 4次;
5.在springboot的控制台看见日志如下:
在这里插入图片描述
可以看出是4个线程同时在执行,执行完4次start&end executeAsync, 总共花费了2秒时间,这就是多线程可以提高程序运行效率的体现。

4.6 获取多线程的返回值
Java自jdk1.5以后,提供了java.util.concurrent.Future来获取异步线程返回的结果。 主线程会创建一个 Future 接口的对象,然后启动并发线程,并告诉并发线程,一旦你执行完毕,就把结果存储在这个 Future 对象里。

一般情况下,我们会把长时间运行的逻辑放在异步线程中进行处理,这是使用 Future 接口最理想的场景。主线程只要简单的将异步任务封装在 Future 里,然后开始等待 Future 的完成,在这段等待的时间内,可以处理一些其它逻辑,一旦 Future 执行完毕,就可以从中获取执行的结果并进一步处理。

AsyncServiceImpl类中增加两个方法:

    //多线程调用并获取回调结果@Async("asyncServiceExecutor")public Future<String> sendMessageAsync1(){log.info("异步发送消息1---执行开始");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info("异步发送消息1---执行结束");return new AsyncResult<>("异步发送消息1");}@Async("asyncServiceExecutor")public Future<String> sendMessageAsync2(){log.info("异步发送消息2---执行开始");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info("异步发送消息2---执行结束");return new AsyncResult<>("异步发送消息2");}

AsyncService类中增加接口:

    Future<String> sendMessageAsync1();Future<String> sendMessageAsync2();

Controller中增加http接口调用:

    //异步多线程调用,但是要等方法回调结果。用多线程,所以只需要2秒@RequestMapping("/sendMessageAsync")public String sendMessageAsync() throws ExecutionException, InterruptedException {System.out.println("开始时间:"+new Date());Future<String> sendMessageAsync1 = asyncService.sendMessageAsync1();Future<String> sendMessageAsync2 = asyncService.sendMessageAsync2();String result="";String result1="";String result2="";while(!(sendMessageAsync1.isDone() && sendMessageAsync2.isDone())){
//            System.out.println(
//                    String.format(
//                            "future1 is %s and future2 is %s",
//                            sendMessageAsync1.isDone() ? "done" : "not done",
//                            sendMessageAsync2.isDone() ? "done" : "not done"
//                    )
//            );
//            Thread.sleep(300);}result +=sendMessageAsync1.get();result +=sendMessageAsync2.get();System.out.println("结束时间:"+new Date());return result;

上面使用的是先调用 Future.isDone() 判断任务是否完成,再调用 Future.get() 从完成的任务中获取任务执行的结果。

也可以直接用Future.get()并设置一个超时时间:

    @RequestMapping("/sendMessageAsync")public String sendMessageAsync() throws ExecutionException, InterruptedException {System.out.println("开始时间:"+new Date());Future<String> sendMessageAsync1 = asyncService.sendMessageAsync1();Future<String> sendMessageAsync2 = asyncService.sendMessageAsync2();String result="";String result1="";String result2="";//通过future.get()方法阻塞性获取执行结果,设置超时时间为3秒,3秒还没获取到值,就超时报错try {result1=sendMessageAsync1.get(3000, TimeUnit.MILLISECONDS);} catch (TimeoutException e) {sendMessageAsync1.cancel(true);log.error("sendMessageAsync1方法超时未返回结果");e.printStackTrace();}try {result2=sendMessageAsync2.get(3000, TimeUnit.MILLISECONDS);} catch (TimeoutException e) {sendMessageAsync2.cancel(true);log.error("sendMessageAsync2方法超时未返回结果");e.printStackTrace();}result=result1+result2;System.out.println("结束时间:"+new Date());return result;}

Future.get() 方法是一个阻塞方法。如果任务还没执行完毕,那么会一直阻塞直到直到任务完成
为了防止调用 Future.get() 方法阻塞当前线程,推荐的做法是先调用 Future.isDone() 判断任务是否完成,然后再调用 Future.get() 从完成的任务中获取任务执行的结果。

因为 Future.isDone() 和 Future.get() 的存在,我们就可以在等待任务完成时运行其它一些代码。使用 isDone() 和 get() 方法来获取结果,这应该是消费 Future 最常见的方式。
针对上面的代码, 如果不用isDone(),直接用get(), 那么get()阻塞的这2秒内就不能做任何其他事情。而用了while isDone(), 这2秒内则可以做一些其他的事情,比如上面代码中的输出打印一段话。

运行结果如下:
在这里插入图片描述
在这里插入图片描述
虽然sendMessageAsync1和sendMessageAsync2都要2秒时间,由于是多线程并行处理,所以总共只花费了2秒。

正常的单线程处理:

    //正常的单线程处理,要花4秒@RequestMapping("/sendmessage")public String sendMessage() throws ExecutionException, InterruptedException {System.out.println(new Date());//调用service层的任务String sendMessage1=asyncService.sendMessage1();String sendMessage2=asyncService.sendMessage2();String result="";result=sendMessage1+sendMessage2;System.out.println(new Date());return result;}

在这里插入图片描述
在这里插入图片描述
单线程顺序执行sendMessage1和sendMessage2,每一个方法执行需要2秒,总共就需要4秒才能执行完。

项目代码已上传至GitHub,需要的朋友可以自行下载: threadpooldemoserver

============================================================================

以上就是本篇文章的全部内容,如果对你有帮助,

欢迎扫码关注程序员杨叔的微信公众号,获取更多全栈测试干货内容资料:
​​​​​在这里插入图片描述


http://chatgpt.dhexx.cn/article/0jZyXZeE.shtml

相关文章

【线程】多线程编程

目录 一、概念 1.进程与线程的区别是&#xff1f;&#xff08;常问&#xff09; 2.线程与fork系统调用 3.线程的优缺点 4.线程的实现方式 二、线程函数 1.pthread_create 2.pthread_exit 3.pthread_join 4.pthread_cancel 三、线程的使用 1.线程的基本操作 2.并发…

多线程编程及线程间通信机制

对进程线程的印象还是比较好的,这对于学习C高级的朋友是非常重要的,怎样更快的学习到线程的具体使用呢?最好的办法自然是练习再练习,然后还要看很多的代码才是,之前又听说了一个IT同行过劳死,特别的提醒广大的IT从业者,要注意合理的作息习惯,健康才是最重要的,下面结合…

多线程编程

多线程指的是一个程序中包含两个或者两个以上的线程&#xff0c;多线程的提出是为提高代码的执行效率&#xff0c;这就好比工厂中的流水线&#xff0c;只有一条称为单线程&#xff0c;有多条流水线就称为多线程。多线程提高效率的同时由于并发执行的不确定性&#xff0c;导致出…

两种 C++ 多线程编程方式,看完不懂打我...

多线程在实际编程中的重要性不言而喻&#xff0c;对 C 而言&#xff0c;当我们需要使用多线程时&#xff0c;有多种方案可供选择。比如 POSIX 线程 pthread、boost::thread 库、C11 开始支持的 std::thread 库&#xff0c;以及其他一些第三方库 libdispatch&#xff08;GCD&…

ARP攻击模拟工具

以下是本人平时的一些小作品&#xff0c;特此和大家一起分享... ARP攻击模拟工具 第一代 说明&#xff1a; 实现模拟以太网的ARP欺骗攻击和IP地址冲突攻击。 此为本人早期作品&#xff0c;固比较简陋和不完善&#xff0c;漏洞及错误之处可能较多请多多包涵&#xff01; 开发环…

ARP断网攻击

在之前的文章当中&#xff0c;我已经向大家介绍了关于ARP的欺骗原理。再进一步&#xff0c;将其中的原理运用到实战中去&#xff0c;又会有什么效果呢&#xff1f;ARP又可以给我们带来哪些具体的作用呢&#xff1f;毕竟实践要来支撑&#xff0c;我们之前学过的ARP欺骗原理。 我…

ARP-欺骗攻击

测试环境&#xff08;kali:192.168.189.7 win7:192.168.189.6 网关:192.168.189.2&#xff09; arpspoof -i eth0 -t 192.168.189.6 192.168.189.2 &#xff08;目标地址网关地址&#xff09; 接收获取的信息 随意登陆一个网站&#xff0c;可发现用户名及密码已被获取。可以…

ARP攻击原理及解决方法,很实用

故障原因】 局域网内有人使用ARP欺骗的木马程序&#xff08;比如&#xff1a;传奇盗号的软件&#xff0c;某些传奇外挂中也被恶意加载了此程序&#xff09;。 【故障原理】 要了解故障原理&#xff0c;我们先来了解一下ARP协议。 在局域网中&#xff0c;通过ARP协议来完成I…

ARP攻击怎么解决最安全

ARP攻击是指攻击者通过伪造网络中的ARP协议数据包&#xff0c;欺骗其他计算机的网络通信&#xff0c;从而实现中间人攻击等目的。 下面是ARP攻击的解决方法&#xff1a; 静态ARP表绑定MAC地址&#xff1a;在网络管理员的控制下&#xff0c;将主机的IP地址与MAC地址进行绑定&am…

ARP攻击及原理

ARP攻击原理:但凡局域网内存在arp攻击&#xff0c;说明网络存在“中间人” 1.PC1需要跟PC2通信&#xff0c;通过ARP请求包询问PC2的MAC地址&#xff0c;由于采用广播的形式&#xff0c;所以交换机会将ARP请求包从接口PC1广播到PC2和PC3。(注&#xff1a;交换机收到广播/组播/未…

ARP攻击、欺骗及防御

一、广播与广播域概述 1、广播与广播域 广播&#xff1a;将广播地址做为目的地址的数据帧。 广播域&#xff1a;网络中能接收到同一个广播所有节点的集合。 2、MAC地址广播 广播地址为FF-FF-FF-FF-FF-FF 3、IP地址广播 1&#xff09;255.255.255.255 2&#xff09;广播…

ARP攻击原理和kali实现ARP攻击

目录 一、ARP协议和ARP攻击1.ARP协议2.利用Wireshark分析ARP数据包3.ARP攻击 二、kali实现ARP攻击和ARP欺骗1.实验过程2.问题记录T_T 一、ARP协议和ARP攻击 1.ARP协议 ARP协议&#xff0c;地址解析协议&#xff08;Address Resolution Protocol&#xff09;&#xff0c;用来实…

怎么处理ARP攻击

ARP故障说明: ping 的时候可能好久才能接收到一个响应&#xff0c;或者说干脆就Ping不通。网络时好时坏&#xff0c;通过ARP -a命令不能看到同网段的在线用户&#xff0c;但是net view 可以看到全网开放共享的电脑名&#xff0c;这时候就需要注意了&#xff0c;可能是ARP攻击。…

网络安全--ARP攻击原理与防护

目录 一.ARP的原理 二.ARP攻击现象及危害 三.ARP攻击的原理 四.ARP防护 在局域网当中&#xff0c;有一个协议由于它的特性一旦遭受攻击就非常麻烦。首先是它的攻击门槛比较低&#xff0c;找到一些小工具就能实现攻击&#xff0c;而且危害极大。这个协议就是ARP协议。 ARP攻击是…

渗透技术——ARP攻击

Part 1: ARP攻击介绍 ARP&#xff08;Address Resolution Protocol&#xff0c;地址解析协议&#xff09;是一个位于TCP/IP协议栈中的底层协议&#xff0c;负责将某个IP地址解析成对应的MAC地址。而ARP攻击就是通过伪造IP地址和MAC地址实现ARP欺骗&#xff0c;能够在网络中产生…

Linux操作系统及其发行版本

文章目录 Linux是什么Liunx与WindowsLinux的发行版本1. Debian系列&#xff1a;2. Slackware系列&#xff1a;3. Redhat系列&#xff1a;4. 其他发行版本&#xff1a; Linux是什么 Linux是一套免费使用和自由传播的类Unix操作系统&#xff0c;是一个基于POSIX和UNIX的多用户、…

linux查看系统版本命令

*一、查看Linux系统发行版本 * 命令1&#xff1a;lsb_release -a 该命令适用于所有Linux系统&#xff0c;会显示出完整的版本信息&#xff0c;包括Linux系统的名称&#xff0c;如Debian、Ubuntu、CentOS等&#xff0c;和对应的版本号&#xff0c;以及该版本的代号&#xff0c…

哪些是linux系统版本,linux系统主要有哪些版本

linux系统主要版本有&#xff1a;Redhat版本&#xff0c;基于RPM包的YUM包管理方式&#xff1b;2.CentOS版本&#xff0c;免费的、开源的、可以重新分发的linux发行版&#xff1b;3.Ubuntu版本&#xff0c;拥有漂亮的用户界面的系统&#xff1b;4.Mandriva版本&#xff0c;KDE桌…

Linux操作系统介绍及版本

1.1 认识Linux Linux操作系统是基于UNIX以网络为核心的设计思想&#xff0c;是一个性能稳定的多用户网络操作系统&#xff0c;Linux能运行各种工具软件、应用程序以及网络协议&#xff0c;它支持安装在32位和64位CPU硬件上。 通常来讲&#xff0c;Linux这个词只表示Linux内核&a…

Linux版本简介

Linux系统最早由LinusTorvalds在1991年开始编写&#xff0c;在此诞生之前同时也具备了五大前提条件&#xff1a;UNIX操作系统、MINIX 操作系统、GNU计划、POSIX标准、INTERNET。先让我们看一下Linux系统的发行版本&#xff0c;包括Redhat、Ubuntu、Fedora、SUSE、Slackware、De…