Hello,大胸弟们,我们又又又见面了,今天攀哥继续为大家分享一下SpringBoot的教程,没点关注的宝宝,点一下关注。
🌲 异步调用简介:
🌿 什么是异步调用?
“异步调⽤”对应的是“同步调⽤”,同步调⽤指程序按照定义顺序依次执⾏,每⼀⾏程序都必须等待上⼀⾏程序执⾏完成之后才能执⾏;异步调⽤指程序在顺序执⾏时,不等待异步调⽤的语句返回结果就执行后⾯的程序。
在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件,后台会去发送邮件。此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务还有一些定时任务,比如需要在每天凌晨的时候,分析前一天的日志信息等等,,,,,,
对于异步操作,SpringBoot提供了一些对应的支持,我们上手十分简单,只需要开启一些注解,配置一些配置文件即可
🌿 同步调用案例
- 定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作小号时效件随机取(10秒内),代码如下:
package com.moxuan.boot_03_async.entity;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class Task {
public static Random random = new Random();
public void doTaskOne() throws Exception{
System.out.println("开始执行任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:"+(end-start)+"毫秒");
}
public void doTaskTwo() throws Exception{
System.out.println("开始执行任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:"+(end-start)+"毫秒");
}
public void doTaskThree() throws Exception{
System.out.println("开始执行任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:"+(end-start)+"毫秒");
}
}
在单元测试⽤例中,注⼊Task对象,并在测试⽤例中执⾏ doTaskOne 、 doTaskTwo 、 doTaskThree 三个函数。
package com.moxuan.boot_03_async;
import com.moxuan.boot_03_async.entity.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Boot03AsyncApplicationTests {
@Autowired
private Task task;
@Test
void contextLoads() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}
}
执⾏单元测试,可以看到类似如下输出:
任务⼀、任务⼆、任务三顺序的执⾏完了,换⾔之 doTaskOne 、 doTaskTwo 、 doTaskThree 三个 函数顺序的执⾏完成。
🌿 异步调用案例
上述的同步调⽤虽然顺利的执⾏完了三个任务,但是可以看到执⾏时间⽐较⻓,若这三个任务本身之 间不存在依赖关系,可以并发执⾏的话,同步调⽤在执⾏效率⽅⾯就⽐较差,可以考虑通过异步调⽤的⽅式来并发执⾏。
在Spring Boot中,我们只需要通过使⽤ @Async 注解就能简单的将原来的同步函数变为异步函数,Task类改在为如下模式:
package com.moxuan.boot_03_async.entity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class Task {
public static Random random = new Random();
@Async
public void doTaskOne() throws Exception{
// 内容同上,省略
}
@Async
public void doTaskTwo() throws Exception{
// 内容同上,省略
}
@Async
public void doTaskThree() throws Exception{
// 内容同上,省略
}
}
为了让@Async注解能够⽣效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:
package com.moxuan.boot_03_async;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class Boot03AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(Boot03AsyncApplication.class, args);
}
}
此时可以反复执⾏单元测试,您可能会遇到各种不同的结果,⽐如:
- 没有任何任务相关的输出
- 有部分任务相关的输出
- 乱序的任务相关的输出
原因是⽬前 doTaskOne 、 doTaskTwo 、 doTaskThree 三个函数的时候已经是异步执⾏了。主程序在异步调⽤之后,主程序并不会理会这三个函数是否执⾏完成了,由于没有其他需要执⾏的内容,所以程序就⾃动结束了,导致了不完整或是没有输出任务相关内容的情况。
注: @Async所修饰的函数不要定义为static类型,这样异步调⽤不会⽣效
🌲 异步回调
为了让 doTaskOne 、 doTaskTwo 、 doTaskThree 能正常结束,假设我们需要统计⼀下三个任务并发执⾏共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。
那么我们如何判断上述三个异步调⽤是否已经执⾏完成呢?我们需要使⽤ Future<T> 来返回异步调⽤的结果,就像如下⽅式改造 三个 函数:
package com.moxuan.boot_03_async.entity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import java.util.Random;
import java.util.concurrent.Future;
@Component
public class Task {
public static Random random = new Random();
@Async
public Future<String> doTaskOne() throws Exception{
System.out.println("开始执行任务一");
long start = System.currentTimeMillis();
System.out.println("start.....");
Thread.sleep(random.nextInt(10000));
System.out.println("end....");
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:"+(end-start)+"毫秒");
return new AsyncResult<>("任务一完成");
}
@Async
public Future<String> doTaskTwo() throws Exception{
System.out.println("开始执行任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:"+(end-start)+"毫秒");
return new AsyncResult<>("任务二完成");
}
@Async
public Future<String> doTaskThree() throws Exception{
System.out.println("开始执行任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:"+(end-start)+"毫秒");
return new AsyncResult<>("任务三完成");
}
}
按照如上⽅式改造⼀下其他两个异步函数之后,下⾯我们改造⼀下测试⽤例,让测试在等待完成三个 异步调⽤之后来做⼀些其他事情。
package com.moxuan.boot_03_async;
import com.moxuan.boot_03_async.entity.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.Future;
@SpringBootTest
class Boot03AsyncApplicationTests {
@Autowired
private Task task;
@Test
void contextLoads() throws Exception {
long start= System.currentTimeMillis();
Future<String> task1 = task.doTaskOne();
Future<String> task2 = task.doTaskTwo();
Future<String> task3 = task.doTaskThree();
while(true){
if(task1.isDone()&& task2.isDone()&&task3.isDone()){
// 三个任务都调用完成,退出循环
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:"+(end-start)+"毫秒");
}
}
看看我们做了哪些改变:
- 在测试⽤例⼀开始记录开始时间
- 在调⽤三个异步函数的时候,返回 Future<String> 类型的结果对象
- 在调⽤完三个异步函数之后,开启⼀个循环,根据返回的 Future<String> 对象来判断三个异步
- 函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断。
- 跳出循环之后,根据结束时间 - 开始时间,计算出三个任务并发执⾏的总耗时。
执⾏⼀下上述的单元测试,可以看到如下结果:
可以看到,通过异步调⽤,让任务⼀、⼆、三并发执⾏,有效的减少了程序的总运⾏时间
🌲 Async 异步调用:自定义线程池
前面我们已经讲过如何使⽤ @Async 注解来实现异步调⽤了。但是,对于这些异步执⾏的控制是我们保障⾃身应⽤健康的基本技能。本⽂我们就来学习⼀下,如何通过⾃定义线程池的⽅式来控制异步调⽤的并发。
🌿 定义线程池
package com.moxuan.boot_03_async.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@EnableAsync
@Configuration
public class TaskPoolConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池创建时候初始化的线程数
executor.setCorePoolSize(10);
// 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(10);
// 缓冲队列:用来缓冲执行任务的队列
executor.setQueueCapacity(200);
// 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("AsyncPool-");
// 缓冲队列满了之后的拒绝策略:由调用线程处理(一般是主线程)
//AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常
//DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态
//DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
//CallerRunsPolicy:不丢弃任务 由调用线程处理该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
executor.setAllowCoreThreadTimeOut(true);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
上⾯我们通过使⽤ ThreadPoolTaskExecutor 创建了⼀个线程池,同时设置了以下这些参数:
- 核⼼线程数10:线程池创建时候初始化的线程数
- 最⼤线程数10:线程池最⼤的线程数,只有在缓冲队列满了之后才会申请超过核⼼线程数的线程缓
- 队列200:⽤来缓冲执⾏任务的队列
- 允许线程的空闲时间60秒:当超过了核⼼线程出之外的线程在空闲时间到达之后会被销毁
- 线程池名的前缀:设置好了之后可以⽅便我们定位处理任务所在的线程池
- 线程池对拒绝任务的处理策略:这⾥采⽤了 CallerRunsPolicy 策略,当线程池没有处理能⼒的时候,该策略会直接在execute⽅法的调⽤线程中运⾏被拒绝的任务;如果执⾏程序已关闭,则会丢弃该任务
🌿 使用线程池
在定义了线程池之后,我们如何让异步调⽤的执⾏任务使⽤这个线程池中的资源来运⾏呢?⽅法⾮常 简单,当配置线程池之后,会自动使用线程池如:
package com.moxuan.boot_03_async.entity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import java.util.concurrent.Future;
/**
* AsyncDemo
*
* @author XiongNeng
* @version 1.0
* @since 2018/1/25
*/
@Component
public class AsyncTask {
private static final Logger logger = LoggerFactory.getLogger(AsyncTask.class);
@Async
public void dealNoReturnTask() {
logger.info("返回值为void的异步调用开始" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("返回值为void的异步调用结束" + Thread.currentThread().getName());
}
@Async
public Future<String> dealHaveReturnTask(int i) {
logger.info("asyncInvokeReturnFuture, parementer=" + i);
Future<String> future;
try {
Thread.sleep(1000 * i);
future = new AsyncResult<String>("success:" + i);
} catch (InterruptedException e) {
future = new AsyncResult<String>("error");
}
return future;
}
}