从零开始 Spring Boot 42:异步执行

从零开始 Spring Boot 42:异步执行

spring boot

图源:简书 (jianshu.com)

在之前的文章中,我多次介绍过在 Spring 中如何使用@Async注解让方法调用变成“异步执行”:

  • 在这篇文章中,介绍了如何让定时任务使用@Async变成异步执行。
  • 在这篇文章中,介绍了如何让事件监听使用@Async变成异步执行。

下面,本篇文章将详细探讨@Async在 Spring 中的用途。

简单示例

老规矩,我们从一个简单示例开始说明:

@Component
public class Fibonacci {
    /**
     * 返回斐波那契数列的第n位的值
     *
     * @param n 从1开始(包括)
     * @return
     */
    public int fibonacci(int n) throws InterruptedException {
        Thread.sleep(100);
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    /**
     * 打印斐波那契数列第n位的结果到控制台
     * @param n 从1开始(包括)
     * @throws InterruptedException
     */
    public void print(int n) throws InterruptedException {
        System.out.printf("fibonacci %d=%d%n", n, fibonacci(n));
    }
}

这里定义一个 bean Fibonacci,负责返回或打印斐波那契数列。

为了让产生斐波那契数列元素的过程“更明显”,这里让每一步递归调用都延迟0.1秒(Thread.sleep(100))。

使用ApplicationRunner测试:

@Configuration
public class WebConfig {
    @Autowired
    private Fibonacci fibonacci;

    @Bean
    public ApplicationRunner applicationRunner() {
        return args -> {
            fibonacci.print(5);
            fibonacci.print(6);
            fibonacci.print(7);
        };
    }
}

输出:

fibonacci 5=5
fibonacci 6=8
fibonacci 7=13

整个测试用例都是顺序执行的,且存在明显的延迟。

可以利用@Async将相应方法的执行改为异步来改善性能:

@Component
public class Fibonacci {
	// ...
    @Async
    public void print(int n) throws InterruptedException {
        System.out.printf("fibonacci %d=%d%n", n, fibonacci(n));
    }
}

@Configuration
@EnableAsync
public class WebConfig {
	// ...
}

不要忘了在配置类上添加@EnableAsync以启用 Spring 的异步执行功能。

实现原理

实际上 Spring 的异步执行是通过使用代理(JDK 代理或 CGLIB)或者 AspectJ 织入来实现的。

AspectJ 是一个主流的 AOP 框架。

这点可以通过@EnableAsync注解的定义看出:

public @interface EnableAsync {
    Class<? extends Annotation> annotation() default Annotation.class;
    boolean proxyTargetClass() default false;
    AdviceMode mode() default AdviceMode.PROXY;
    int order() default 2147483647;
}

这些属性有如下用途:

  • annotation,指定用于标记异步执行方法的注解,默认情况下 Spring 使用@Asyncjavax.ejb.Asynchronous
  • mode,实现机制,有两个可选项:
    • AdviceMode.PROXY,用代理实现。
    • AdviceMode.ASPECTJ,用 AspectJ 实现。
  • proxyTargetClass,是否使用 CGLIB 代理,这个属性只有modeAdviceMode.PROXY时才生效。
  • order,设置AsyncAnnotationBeanPostProcessorBeanPostProcessor中的执行顺序,默认为最后运行,以便不影响之前可能存在的代理。

我们可以看出,默认情况下 Spring 使用 JDK 代理来实现异步调用,因此它也具备 Spring AOP 相同的限制。

AOP 实现

为了更好的说明问题,我们可以用 AOP 来自己实现一个类似的异步执行机制:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAsync {
}

@Component
@Aspect
public class MyAsyncAspect {
    @Around(value = "execution(void *(..)) && @annotation(annotation)")
    public Object asyncCall(ProceedingJoinPoint pjp, MyAsync annotation) {
        new Thread(() -> {
            try {
                pjp.proceed();
            } catch (Throwable e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }).start();
        return null;
    }
}

@Component
public class Fibonacci {
	// ...
    @MyAsync
    public void print(int n) throws InterruptedException {
        System.out.printf("fibonacci %d=%d%n", n, fibonacci(n));
    }
}

更多关于 AOP 的内容,可以阅读我的另一篇文章。

限制

在学习 AOP 的时候,我们知道因为 AOP 的实现机制的关系,存在着一些限制。而 Spring 异步执行采用和 Spring AOP 类似的实现原理,所以也存在同样的问题。

借鉴前边学到的内容,我们很容易就能总结出以下限制:

在默认情况下,异步执行使用 JDK 动态代理实现,因此:

  • 只能让public的方法异步执行(JDK 动态代理使用接口实现)。
  • “自调用”时可能无法异步执行(绕过代理)。

如果使用 CGLIB 代理实现,限制会相对少一些(可以代理protected方法),但依然存在自调用时的问题。

关于此类限制的讨论和相应的解决方案,可以阅读 AOP 相关的文章,里边有详细描述,这里不再赘述。

返回结果

通常情况下异步执行方法返回的都是void,但如果我们需要返回异步执行的结果,要怎么做?

看一个示例:

@Configuration
@EnableAsync
public class WebConfig {
    @Autowired
    private Fibonacci fibonacci;
    private static final int MAX_FIBONACCI_INDEX = 40;

    @Bean
    ApplicationRunner applicationRunner2() throws InterruptedException {
        return new ApplicationRunner() {
            @Override
            @MyClock
            public void run(ApplicationArguments args) throws Exception {
                List<Integer> numbers = new ArrayList<>();
                for (int n = 1; n <= MAX_FIBONACCI_INDEX; n++) {
                    numbers.add(fibonacci.fibonacci(n));
                }
                System.out.println(numbers);
            }
        };
    }
}

这里获取40个斐波那契元素,然后一起输出。因为其中每次获取斐波那契数都是顺序执行(单线程),所以相当耗时。

最终输出:

[1, 1, 2, ... , 63245986, 102334155]
com.example.async.WebConfig$2.run() is called, use 876 mills.

下面我们用异步执行来改善效率。

要让方法异步执行并返回一个值,需要让方法返回一个Future类型:

@Component
public class Fibonacci {
	// ...
    @Async
    public Future<Integer> asyncFibonacci(int n) throws InterruptedException {
        int result = fibonacci(n);
        return CompletableFuture.completedFuture(result);
    }
}

这里的CompletableFuture是 Spring 的一个Future实现,可以利用CompletableFuture.completedFuture返回一个包含异步调用结果的Future对象。

最终,我们需要收集所有异步执行返回的Future对象,并通过Future.get方法获取其中的异步执行结果:

@Configuration
@EnableAsync
public class WebConfig {
	// ...
    @Bean
    public ApplicationRunner applicationRunner() {
        return new ApplicationRunner() {
            @Override
            @MyClock
            public void run(ApplicationArguments args) throws Exception {
                List<Integer> numbers = new ArrayList<>();
                List<Future<Integer>> futures = new ArrayList<>();
                for (int n = 1; n <= MAX_FIBONACCI_INDEX; n++) {
                    futures.add(fibonacci.asyncFibonacci(n));
                }
                for (Future<Integer> future : futures) {
                    numbers.add(future.get());
                }
                System.out.println(numbers);
            }
        };
    }
    // ...
}

输出:

[1, 1, 2, ... , 63245986, 102334155]
com.example.async.WebConfig$1.run() is called, use 380 mills.

效率提升了一倍多。

并发相关的经验告诉我们,将并发用于密集计算,计算规模(并行任务数目)越大,性能提升越明显。

ThreadPoolTaskExecutor

默认情况下,Spring 使用ThreadPoolTaskExecutor执行异步方法:

@Configuration
@EnableAsync
public class WebConfig {
	// ...
    @Autowired
    private TaskExecutor taskExecutor;
    // ...
    @Bean
    public ApplicationRunner applicationRunner3(){
        return args -> {
            System.out.println(taskExecutor);
            if (taskExecutor instanceof ThreadPoolTaskExecutor){
                var executor = (ThreadPoolTaskExecutor) taskExecutor;
                System.out.println("getThreadNamePrefix:%s".formatted(executor.getThreadNamePrefix()));
                System.out.println("getActiveCount:%s".formatted(executor.getActiveCount()));
                System.out.println("getCorePoolSize:%s".formatted(executor.getCorePoolSize()));
                System.out.println("getKeepAliveSeconds:%s".formatted(executor.getKeepAliveSeconds()));
                System.out.println("getMaxPoolSize:%s".formatted(executor.getMaxPoolSize()));
                System.out.println("getQueueCapacity:%s".formatted(executor.getQueueCapacity()));
                System.out.println("getPoolSize:%s".formatted(executor.getPoolSize()));
            }
        };
    }
}

输出:

getThreadNamePrefix:task-
getActiveCount:0
getCorePoolSize:8
getKeepAliveSeconds:60
getMaxPoolSize:2147483647
getQueueCapacity:2147483647
getPoolSize:8

ThreadPoolTaskExecutor的这些 Getter 返回的信息包括:

  • getThreadNamePrefix,线程名称前缀。
  • getActiveCount,当前存活的线程数量。
  • getCorePoolSize,核心线程池大小(超过该值后会扩充线程池,直到最大线程池大小)。
  • getMaxPoolSize,最大线程池大小(超过该值后会将线程放入等待队列)。
  • getQueueCapacity,等待队列的容量(被塞满后新的线程将被丢弃)。
  • getKeepAliveSeconds,线程存活数目。
  • getPoolSize,当前线程池大小。

总的来说,``ThreadPoolTaskExecutor`可以合理地复用线程:如果所需线程数目超过核心线程池大小,会将线程放入等待队列,以等待核心线程空闲后执行。如果等待队列被塞满,会添加新的线程以期望能够加快线程执行。最后,如果添加的线程数目超过最大线程池大小,才会按照规则丢弃线程。

这个过程可以用下图表示:

img

图源:知乎

  • 在早期的 Spring 版本,默认使用simpleAsyncTaskExecutor执行异步调用,该TaskExecutor不会进行线程复用,只是简单的增加新的线程。
  • 这里比较重要的是核心线程池大小,一般来说设置为执行代码所在机器的CPU核心数即可,我的笔记本是8核的,所以这里 Spring 将该值设置为8。

一般来说,使用默认设置的ThreadPoolTaskExecutor就可以了,如果需要进行修改,可以:

@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setThreadNamePrefix("ThreadPoolTaskExecutor-");
        threadPoolTaskExecutor.setCorePoolSize(8);
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

此时在异步方法中打印线程名称:

@Component
public class Fibonacci {
	@Async
    public Future<Integer> asyncFibonacci(int n) throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        // ...
    }
    // ...
}

就能看到控制台输出的线程名称是ThreadPoolTaskExecutor-x,而不是之前默认的task-x

单独指定 Executor

我们也可以为某些异步方法单独指定一个Executor,而不是使用全局的Executor

@Configuration
@EnableAsync
public class WebConfig {
	// ...
    @Bean
    public Executor threadPoolTaskExecutor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setThreadNamePrefix("another-ThreadPoolTaskExecutor-");
        return threadPoolTaskExecutor;
    }
}

@Component
public class Fibonacci {
    // ...
    @Async("threadPoolTaskExecutor")
    public Future<Integer> asyncFibonacci(int n) throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
		// ...
    }
    // ...
}

就像上面的示例,可以在@Async中指定一个Executor类型的 bean,Spring 将用这个 bean 执行这个方法的异步调用。

异常处理

如果异常方法返回的是Future,且异步调用会产生异常,将通过Future.get抛出:

@Component
public class Fibonacci {
    // ...
    @Async
    public Future<Integer> asyncFibonacci(int n) throws InterruptedException {
        if (n < 1) {
            throw new IllegalArgumentException("n 不能小于1");
        }
		// ...
    }
    // ...
}

@Configuration
@EnableAsync
public class WebConfig {
    // ...
    @Bean
    public ApplicationRunner applicationRunner3() {
        return args -> {
            Future<Integer> future = fibonacci.asyncFibonacci(0);
            System.out.println(future.get());
        };
    }
}

这里会抛出一个IllegalStateException异常。

如果返回类型是void,Spring 会使用一个默认的“异常处理器”SimpleAsyncUncaughtExceptionHandler来处理异常:

@Component
public class Fibonacci {
	// ...
    @Async
    public void print(int n) throws InterruptedException {
        if (n < 1) {
            throw new IllegalArgumentException("n不能小于1");
        }
        System.out.printf("fibonacci %d=%d%n", n, fibonacci(n));
    }
}

@Configuration
@EnableAsync
public class WebConfig {
    @Bean
    public ApplicationRunner applicationRunner3() {
        return args -> {
            fibonacci.print(0);
        };
    }
}

错误信息:

2023-06-16T16:52:17.509+08:00 ERROR 27872 --- [lTaskExecutor-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void com.example.async.Fibonacci.print(int) throws java.lang.InterruptedException
...

可以用一个自定义异常处理器作为 Spring 异步调用时的全局异常处理器:

@Configuration
public class AsyncConfig implements AsyncConfigurer {
    // ...
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler() {
            @Override
            public void handleUncaughtException(Throwable ex, Method method, Object... params) {
                System.out.println("Exception message - " + ex.getMessage());
                System.out.println("Method name - " + method.getName());
                for (Object param : params) {
                    System.out.println("Parameter value - " + param);
                }
            }
        };
    }
}

The End,谢谢阅读。

本文的完整示例可以通过这里获取。

参考资料

  • 从零开始 Spring Boot 40:定时任务 - 红茶的个人站点 (icexmoon.cn)
  • 从零开始 Spring Boot 41:事件 - 红茶的个人站点 (icexmoon.cn)
  • 从零开始 Spring Boot 32:AOP II - 红茶的个人站点 (icexmoon.cn)
  • AsyncResult (Spring Framework 6.0.10 API) — AsyncResult(Spring Framework 6.0.10 API)
  • SimpleAsyncTaskExecutor (Spring Framework 6.0.10 API)
  • Spring自带的线程池ThreadPoolTaskExecutor - 知乎 (zhihu.com)
  • How To Do @Async in Spring | Baeldung

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——图

文章目录 **一 图的基本概念****1 定义** **二 图的存储及基本操作****1 邻接矩阵法****2 邻接表法****3 十字链表****4 邻接多重表****5 图的基本操作** **三 图的遍历****1 广度优先搜索BFS****2 深度优先搜索DFS****3 图的遍历与连通性** **四 图的应用****1 最小生成树****…

Kendo UI for jQuery---03.组件___网格---02.开始

网格入门 本指南演示了如何启动和运行 Kendo UI for jQuery Grid。 完成本指南后&#xff0c;您将能够实现以下最终结果&#xff1a; 1. 创建一个空的 div 元素 首先&#xff0c;在页面上创建一个空元素&#xff0c;该元素将用作 Grid 组件的主容器。 <div id"my-…

Chat2DB:阿里巴巴开源的聊天数据管理工具--实践

Chat2DB&#xff1a;阿里巴巴开源的聊天数据管理工具–实践 简介 ​ Chat2DB 是一款有开源免费的多数据库客户端工具&#xff0c;支持windows、mac本地安装&#xff0c;也支持服务器端部署&#xff0c;web网页访问。和传统的数据库客户端软件Navicat、DBeaver 相比Chat2DB集成了…

Android音视频开发实战01-环境搭建

一,FFmpeg介绍 FFmpeg 是一款流行的开源多媒体处理工具&#xff0c;它可以用于转换、编辑、录制和流式传输音视频文件。FFmpeg 具有广泛的应用场景&#xff0c;包括视频编解码、格式转换、裁剪、合并、滤镜等等。官网:https://ffmpeg.org/ FFmpeg 支持各种常见的音视频格式&a…

计算机视觉-目标检测(二):从R-FCN到YOLO-v3

文章目录 1. R-FCN1.1 动机1.2. R-FCN 网络结构1.3. R-FCN 的损失函数1.4. R-FCN的训练及性能 2. YoLO-v12.1 简介2.2 YOLO-v1网络结构2.3 目标函数2.4 YOLO-v1的优缺点 3. YOLO-v23.1 YOLO-v2相比v1的优化 4. YOLO-v3参考 1. R-FCN 论文链接&#xff1a;R-FCN:Object Detecti…

在提交代码时有哪些注意事项

分享 10 种适合初学者的技术&#xff0c;这些技术将帮助您立即编写更好的代码。因此&#xff0c;如果您准备好将您的编码技能提升到一个新的水平&#xff0c;请继续阅读&#xff01; 1. 从计划开始 编写更好代码的最佳方法之一是从计划开始。在开始编码之前&#xff0c;请花几…

Nginx+Tomcat负载均衡、动静分离

Nginx应用 Nginx是一款非常优秀的HTTP服务器软件 支持高达50000个并发连接数的响应拥有强大的静态资源处理能力运行稳定内存、CPU等系统资源消耗非常低 目前很多大型网站都应用Nginx服务器作为后端网站程序的反向代理及负载均衡器&#xff0c;提升整个站点的负载并发能力 反向…

STM32 Proteu直流电机正反转控制系统限位开关-0035

STM32 Proteu直流电机正反转控制系统限位开关-0035 Proteus仿真小实验&#xff1a; STM32 Proteu直流电机正反转控制系统限位开关-0035 功能&#xff1a; 硬件组成&#xff1a;STM32F103C6单片机 L298N电机控制电路直流电机3个按键&#xff08;正转、反转、停止&#xff09;L…

Java企业级信息系统开发学习笔记(4.3) Spring Boot两种全局配置和两种注解

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【http://t.csdn.cn/Z2viP】 文章目录 一、Application.properites配置文件1.1 创建Spring Boot的Web项目ProperitesDemo1.2 在应用属性文件里添加相关配置1. 配置服务器端口号和web虚拟路径2. 对象类型的配置与使用&…

【6.09 代随_52day】 最长递增子序列、最长连续递增序列、最长重复子数组

最长递增子序列、最长连续递增序列、最长重复子数组 最长递增子序列1.方法图解步骤递归代码 最长连续递增序列1.动态规划的方法图解步骤代码 最长重复子数组图解步骤代码 最长递增子序列 力扣连接&#xff1a;300. 最长递增子序列&#xff08;中等&#xff09; 1.方法 dp[i]…

【计算机网络自顶向下】如何学好计网-第二章应用层

第二章 应用层 应用层协议原理 网络应用程序体系结构 客户机/服务器体系结构&#xff1a;至少有一个服务器&#xff0c;一个客户机&#xff0c;其中服务器总是打开的&#xff0c;具有固定的众所周知的IP地址&#xff0c;主机群集常被用于创建强大的虚拟服务器&#xff0c;而客…

开发新项目看过来,这3款基于 Vue 的免费开源的 admin 管理后台框架非常好用

三款 admin 框架&#xff0c;分别基于热门的前端 UI 组件库 ElementPlus / Ant Design / Naive UI 打造&#xff0c;开箱即用。 新项目的开始&#xff0c;一般是搭建 admin 系统&#xff0c;今天盘点一下3个好的选择。 Vue vben admin 了解详细&#xff1a;https://www.thos…

数据建模学习2--作业-利用matlab解决实际问题

文章目录 Malthus模型问题用最小儿二乘法估计阻滞增长模型自来水运输问题利用 Dijkstra 算法计算下图中起点 D 至各顶点得最短距离&#xff0c;需要给出 仅供参考&#xff0c;代码注意修改 Malthus模型问题 1790-1980年间美国每隔10年的人口数量记录如下表所示。 表1 1790-1…

虚拟机(VMware )部署

一、VMware 概述&#xff1a; VMware是一家提供虚拟化解决方案的领先公司&#xff0c;其产品被广泛应用于企业和个人用户的计算环境中。VMware的虚拟化技术可以将物理计算资源&#xff08;如服务器、存储和网络&#xff09;抽象成虚拟化的资源&#xff0c;从而提供更高的灵活性…

kali学习笔记(二)

一、关闭自动锁屏 关闭自动锁屏对于测试人员来说&#xff0c;可以按照自己的习惯来设置&#xff0c;不然kali会过十分钟就锁屏&#xff0c;有的时候会比较不方便。 1、使用root账号登录&#xff0c;在display设置选项中做如下设置。 2、把休眠选项关掉。 二、创建快照 关机创…

算法刷题-数组-长度最小的子数组

209.长度最小的子数组 力扣题目链接 给定一个含有 n 个正整数的数组和一个正整数 s &#xff0c;找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组&#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0。 示例&#xff1a; 输入&#xff1a;s 7, …

React-Redux 对Todolist修改

在单独使用redux的时候 需要手动订阅store里面 感觉特别麻烦 不错的是react有一个组件可以帮我们解决这个问题, 那就是react-redux。 react-redux提供了Provider 和 connent给我们使用。 先说一下几个重点知道的知识 Provider 就是用来提供store里面的状态 自动getState()co…

MySQL——视图(VIEW)详解

今天我们一起来学起视图(VIEW)&#xff0c;那么视图是什么呢&#xff1f;视图有什么作用呢&#xff1f;视图一方面可以帮我们使用表的一部分而不是所有的表&#xff0c;另一方面也可以针对不同的用户制定不同的查询视图&#xff01;带着问题一起来寻找答案吧~~~ 1. 常见的数据库…

Zookeeper概述

​ ZooKeeper概述 ZooKeeper是什么 zookeeper是一个为分布式应用程序提供的一个分布式开源协调服务框架。是Google的Chubby的一个开源实现&#xff0c;是Hadoop和Hbase的重要组件。主要用于解决分布式集群中应用系统的一致性问题。提供了基于类似Unix系统的目录节点树方式的数…

python3 爬虫相关学习9:BeautifulSoup 官方文档学习

目录 1 BeautifulSoup 官方文档 2 用bs 和 requests 打开 本地html的区别&#xff1a;代码里的一段html内容 2.1 代码和运行结果 2.2 用beautiful 打开 本地 html 文件 2.2.1 本地html文件 2.2.2 soup1BeautifulSoup(html1,"lxml") 2.3 用requests打开 本地 h…