SpringMvc使用@Async注解实现有返回值和无返回值的异步处理

1. SpringMvc使用@Async注解实现有返回值和无返回值的异步处理

异步调用对应的是同步调用,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

1.1 同步方式调用代码
  • 相关代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Service
    public class TaskService {

    public void doTaskOne() throws Exception {
    System.out.println("开始做任务一");
    long start = System.currentTimeMillis();
    Thread.sleep(2000);
    long end = System.currentTimeMillis();
    System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
    }

    public void doTaskTwo() throws Exception {
    System.out.println("开始做任务二");
    long start = System.currentTimeMillis();
    Thread.sleep(3000);
    long end = System.currentTimeMillis();
    System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
    }

    public void doTaskThree() throws Exception {
    System.out.println("开始做任务三");
    long start = System.currentTimeMillis();
    Thread.sleep(4000);
    long end = System.currentTimeMillis();
    System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
    }

    }
  • 同步调用

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private TaskService task;

public String test() {
try {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}catch (Exception e){
}
}

下面是运行结果,可以看到三个方法是依次执行的,分别耗时2秒、3秒、4秒、总耗时9秒

1
2
3
4
5
6
开始做任务一
完成任务一,耗时:2001毫秒
开始做任务二
完成任务二,耗时:3000毫秒
开始做任务三
完成任务三,耗时:4001毫秒

上面的同步调用,虽然顺利地完成了三个任务,但是执行时间比较长,如果这三个任务没有依赖关系,可以并发执行的话,可以考虑使用异步调用的方法。

2. 异步方式调用代码无返回值
  • 首先在spring中配置相关参数开启异步调用
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">

<task:annotation-driven executor="taskExecutor" />
<task:executor id="taskExecutor" pool-size="20" queue-capacity="1000"/>
</beans>

如果直接按照下面的方式配置,则 Spring 会使用默认的线程池 org.springframework.core.task.SimpleAsyncTaskExecutor
但这个 SimpleAsyncTaskExecutor 不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<task:annotation-driven/>

# 详见spring-context-4.1.7.RELEASE.jar/org/springframework/scheduling/config/spring-task-4.0.xsd 的描述
<xsd:attribute name="executor" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
Specifies the java.util.Executor instance to use when invoking asynchronous methods.
If not provided, an instance of org.springframework.core.task.SimpleAsyncTaskExecutor
will be used by default.
Note that as of Spring 3.1.2, individual @Async methods may qualify which executor to
use, meaning that the executor specified here acts as a default for all non-qualified
@Async methods.
]]></xsd:documentation>
</xsd:annotation>
  • 在方法上加上 @Async 注解就能将同步函数变成异步函数,改造后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.concurrent.Future;

@Service
public class TaskService {
@Async
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(2000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
}
@Async
public void doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(3000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
}
@Async
public void doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(4000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
}

}
  • 重新调用之后的运行结果如下:
1
2
3
4
5
6
开始做任务三
开始做任务二
开始做任务一
完成任务一,耗时:2000毫秒
完成任务二,耗时:3001毫秒
完成任务三,耗时:4000毫秒

注意事项

  • @Async 所修饰的函数不要定义为 static 类型,这样异步调用不会生效。

  • 调用方法和异步函数不能在一个 class 中。

  • 可以在使用的时候自定义线程池 @Async("poolTaskExecutor")

3. 异步方式调用代码有返回值

如果想知道异步函数什么时候执行完,那就需要使用 Future (AsyncResult是Future的子类)来返回异步调用的结果。
改造后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
public class TaskService {

@Async
public Future<String> doTaskOne() {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(2000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}

@Async
public Future<String> doTaskTwo() {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(3000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
return new AsyncResult<>("任务二完成");
}

@Async
public Future<String> doTaskThree() {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(4000);
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:%s" , (end - start) + "毫秒");
return new AsyncResult<>("任务三完成");
}

}
  • 异步方式调用代码有返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Autowired
private TaskService task;

public List<String> test() {
List<String> result = new ArrayList(16);
StopWatch stopWatch = new StopWatch();
stopWatch.start("接口速度统计");
// 3秒超时
int timeout = 3;
try {
Future<String> task1 = task.doTaskOne();
Future<String> task2 = task.doTaskTwo();
Future<String> task3 = task.doTaskThree();

String taskString1= task1.get(timeout, TimeUnit.SECONDS);
String taskString2= task2.get(timeout, TimeUnit.SECONDS);
String taskString3= task3.get(timeout, TimeUnit.SECONDS);

result.add(taskString1);
result.add(taskString2);
result.add(taskString3);

}catch (TimeoutException | InterruptedException | ExecutionException e){
return result;
} finally {
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

开始做任务三
开始做任务二
开始做任务一
完成任务一,耗时:2001毫秒
完成任务二,耗时:3000毫秒
完成任务三,耗时:4001毫秒

StopWatch '': running time (millis) = 248
-----------------------------------------
ms % Task name
-----------------------------------------
04036 100% 接口速度统计

刚开始想利用CountDownLatch来实现等待所有线程结束整合结果,后来调整为 Future 的 get(long timeout, TimeUnit unit) 来实现线程的超时控制,我看有些的例子使用死循环来阻塞整合线程的执行结果,这样做是有些问题的,如果有个线程一直没有结束运行,那就完犊子了!

这是一种常见的场景将一个大的任务切分为数个子任务,并行处理所有子任务,当所有子任务都成功结束时再继续处理后面的逻辑。还有一种做法是利用CountDownLatch, 主线程构造countDownLatch对象,latch的大小为子任务的总数,每一个任务持有countDownLatch的引用,任务完成时对latch减1,主线程阻塞在countDownLatch.await方法上,当所有子任务都成功执行完后,latch=0, 主线程继续执行。

总结

异步调用可以提升接口性能。比如导出下载、发送邮件短信等代码,可以使用异步执行。

参考 https://blog.csdn.net/qqfo24/article/details/81383022

  • 作者: Sam
  • 发布时间: 2020-01-03 23:12:21
  • 最后更新: 2020-10-02 18:34:35
  • 文章链接: https://ydstudios.gitee.io/post/e7e78673.html
  • 版权声明: 本网所有文章除特别声明外, 禁止未经授权转载,违者依法追究相关法律责任!