点击上方“Java基基”,选择“设为星标”
做积极的人,而不是积极废人!
每天 14:00 更新文章,每天掉亿点点头发...
源码精品专栏
原创 | Java 2021 超神之路,很肝~
中文详细注释的开源项目
RPC 框架 Dubbo 源码解析
网络应用框架 Netty 源码解析
消息中间件 RocketMQ 源码解析
数据库中间件 Sharding-JDBC 和 MyCAT 源码解析
作业调度中间件 Elastic-Job 源码解析
分布式事务中间件 TCC-Transaction 源码解析
Eureka 和 Hystrix 源码解析
Java 并发源码
来源:chuckfang.com/2019/11/13/Async
在实际的项目中,对于一些用时比较长的代码片段或者函数,我们可以采用异步的方式来执行,这样就不会影响整体的流程了。比如我在一个用户请求中需要上传一些文件,但是上传文件的耗时会相对来说比较长,这个时候如果上传文件的成功与否不影响主流程的话,就可以把上传文件的操作异步化,在spring boot中比较常见的方式就是把要异步执行的代码片段封装成一个函数,然后在函数头使用@Async注解,就可以实现代码的异步执行(当然首先得在启动类上加上@EnableAsync注解了)。
具体的使用方式这里我也就不再演示了,网上教大家使用@Async的很多。今天我要讲的并不是怎么去使用@Async注解,而是讲我在实际开发过程中遇到的一个坑,希望你不要再犯。
首先,再明确一点,学习一个知识,第一步是找到相应的官网或是比较权威的网站。
那么这个坑是什么呢?就是如果你在同一个类里面调用一个自己的被@Async修饰的函数时,这个函数将不会被异步执行,它依然是同步执行的!所以你如果没有经过测试就想当然的以为只要在方法头加上@Async就能达到异步的效果,那么你很有可能会得到相反的效果。这个是很要命的。
所以我来给你们演示一下,这个效果是多么恐怖。为什么说它恐怖,是因为在程序员的眼中,一切不符合期望的行为都是bug,bug能不恐怖吗?
首先我们先看一个正确使用的方式,建一个spring boot项目,如果你是用Intellij IDEA新建的项目,记得勾上web的依赖。
项目建好后,我们在启动类上加上@EnableAsync注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncdemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncdemoApplication.class, args);
}
}
然后再新建一个类Task,用来放三个异步任务doTaskOne、doTaskTwo、doTaskThree:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
/**
* @author https://www.chuckfang.top
* @date Created on 2019/11/12 11:34
*/
@Component
public class Task {
public static Random random = new Random();
@Async
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) + "毫秒");
}
@Async
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) + "毫秒");
}
@Async
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,在测试用例上测试这三个方法的执行过程:
@SpringBootTest
class AsyncdemoApplicationTests {
public static Random random = new Random();
@Autowired
Task task;
@Test
void contextLoads() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
Thread.sleep(10000);
}
}
为了让这三个方法执行完,我们需要再单元测试用例上的最后一行加上一个延时,不然等函数退出了,异步任务还没执行完。
我们启动看看效果:
❝
开始做任务三 开始做任务二 开始做任务一 完成任务一,耗时:4922毫秒 完成任务三,耗时:6778毫秒 完成任务二,耗时:6960毫秒
❞
我们看到三个任务确实是异步执行的,那我们再看看错误的使用方法。
我们在测试类里面把这三个函数再写一遍,并在测试用例上调用测试类自己的方法:
@SpringBootTest
class AsyncdemoApplicationTests {
public static Random random = new Random();
@Test
void contextLoads() throws Exception {
doTaskOne();
doTaskTwo();
doTaskThree();
Thread.sleep(10000);
}
@Async
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) + "毫秒");
}
@Async
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) + "毫秒");
}
@Async
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) + "毫秒");
}
}
我们再看看效果:
❝
开始做任务一 完成任务一,耗时:9284毫秒 开始做任务二 完成任务二,耗时:8783毫秒 开始做任务三 完成任务三,耗时:943毫秒
❞
它们竟然是顺序执行的!也就是同步执行,并没有达到异步的效果,这要是在生产上使用,岂不凉凉。
这种问题如果不进行测试还是比较难发现的,特别是你想要异步执行的代码并不会执行太久,也就是同步执行你也察觉不出来,或者说你根本发现不了它是不是异步执行。这种错误也很容易犯,特别是当你把一个类里面的方法提出来想要异步执行的时候,你并不会想着新建一个类来放这个方法,而是会在当前类上直接抽取为一个方法,然后在方法头上加上@Async注解,你以为这样就完事了,其实并没有起到异步的作用!我也是在改进我们项目的文件上传时才发现这个问题的。因为文件上传也不会花费太久,所以真的很隐蔽。
其实@Async的这个性质在官网上已经有过说明了,官网:https://www.baeldung.com/spring-async
是这样说的:
❝
First – let’s go over the rules – @Async has two limitations:
it must be applied to public methods only
self-invocation – calling the async method from within the same class – won’t work
The reasons are simple – 「the method needs to be *public*」 so that it can be proxied. And 「self-invocation doesn’t work」 because it bypasses the proxy and calls the underlying method directly.
❞
文章在一开始就提到了@Async的两个限制,其中第二个就是调用自己类上的异步方法是不起作用的。下面也讲了原因,就是这种使用方式绕过了代理而直接调用了方法,所以肯定是同步的了。从这里,我们也知道了另外一个知识点,就是@Async注解其实是通过代理的方式来实现异步调用的。
上面这个错误使用方法,我目前没有在网上看到过有人说明,甚至在程序员DD的博客中也没有对此进行说明,我深表遗憾。希望你看完我的博客之后不要再犯同样的错误了,或者你赶快检查一下你自己的项目中有没有这样使用@Async注解的。如果觉得文章不错,可以推荐给同事看哦,提醒他们正确使用@Async。
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)