spring mvc 异步调用 @Async

定义

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

同步调用

通过一个例子,来看下同步调用。 写了一个 TaskService ,里面有三个方法,分别模拟耗时2秒、3秒、4秒的操作。

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("完成任务一,耗时:" + (end - start) + "毫秒");
}

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

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

}

写一个 service ,调用这三个方法。

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
14:38:13,846 INFO  [stdout] (default task-8) 开始做任务一
14:38:15,848 INFO [stdout] (default task-8) 完成任务一,耗时:2001毫秒
14:38:15,850 INFO [stdout] (default task-8) 开始做任务二
14:38:18,850 INFO [stdout] (default task-8) 完成任务二,耗时:3000毫秒
14:38:18,852 INFO [stdout] (default task-8) 开始做任务三
14:38:22,853 INFO [stdout] (default task-8) 完成任务三,耗时:4001毫秒

异步调用

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

在方法上加上 @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
31
@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("完成任务一,耗时:" + (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("完成任务二,耗时:" + (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("完成任务三,耗时:" + (end - start) + "毫秒");
}

}

另外需要在application.xml中开启异步调用。

1
2
3
4
<task:annotation-driven executor="targetExecutor" scheduler="targetScheduler"/>

<task:executor id="targetExecutor" pool-size="5"/>
<task:scheduler id="targetScheduler" pool-size="10"/>

调用方法不用改动,运行后结果如下:

1
2
3
4
5
6
14:49:16,001 INFO  [stdout] (targetExecutor-1) 开始做任务三
14:49:16,259 INFO [stdout] (targetExecutor-5) 开始做任务二
14:49:16,260 INFO [stdout] (targetExecutor-4) 开始做任务一
14:49:18,261 INFO [stdout] (targetExecutor-4) 完成任务一,耗时:2000毫秒
14:49:19,260 INFO [stdout] (targetExecutor-5) 完成任务二,耗时:3001毫秒
14:49:20,002 INFO [stdout] (targetExecutor-1) 完成任务三,耗时:4000毫秒

可以看到3个任务异步执行,总耗时4秒。

注意

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

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

异步回调

如果想知道异步函数什么时候执行完,那就需要使用 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() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(2000);
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(3000);
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(4000);
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务三完成");
}

}

调用代码如下,增加了一个计算总耗时的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
long start = System.currentTimeMillis();

try {
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(100);
}

long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
}catch (Exception e){
}

运行结果如下:

1
2
3
4
5
6
7
15:21:05,676 INFO  [stdout] (targetExecutor-4) 开始做任务三
15:21:05,676 INFO [stdout] (targetExecutor-3) 开始做任务二
15:21:05,677 INFO [stdout] (targetExecutor-2) 开始做任务一
15:21:07,678 INFO [stdout] (targetExecutor-2) 完成任务一,耗时:2001毫秒
15:21:08,677 INFO [stdout] (targetExecutor-3) 完成任务二,耗时:3000毫秒
15:21:09,677 INFO [stdout] (targetExecutor-4) 完成任务三,耗时:4001毫秒
15:21:09,704 INFO [stdout] (default task-21) 任务全部完成,总耗时:4036毫秒

总结

异步调用可以让一些和主要逻辑无关的代码异步执行,以提升性能。比如一些日志的代码、发送邮件短信等代码,可以使用异步执行。