0%

Spring之异步任务实践

一般情况下,代码中的方法都是顺序执行,下一行代码的方法调用(method B)必须等上一行的方法(method A)执行完成之后才会执行,但是如果method A的执行耗时很长,而且其结果又不对后续的方法产生影响,则可以通过异步调用的方式来执行它,使得整个方法流程不必因等待method A而造成阻塞,在Spring Boot中,提供 @Async 注解来让开发者可以快捷高效地使用该异步调用

abstract.png

同步调用

同步调用,在开发中最为常见,即方法1中的 代码/方法调用 均顺序执行,该方式会因中间某个方法(比如方法2)耗费过长时间而使得整个方法(即,方法1)的产生阻塞导致执行时间过长,尤其是在客户端请求时,服务端给出响应的速度会大大降低,十分影响客户端体验。

这里给出一个同步调用的示例来详细说明, 我们先写一个任务类SyncTask,其下有2个任务需要执行:runTask1,runTask2

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
@Component
public class SyncTask {
public void runTask1() {
try {
System.out.println("Start Task 1 ...");
long start = System.currentTimeMillis();
int second = 10 * 1000;
Thread.sleep(second);
long end = System.currentTimeMillis();
System.out.println("Over Task 1, Run Time: " + (end-start) + "ms");
} catch (Exception e) {
System.out.println("Exit Task 1");
}
}

public void runTask2() {
try {
System.out.println("Start Task 2 ...");
long start = System.currentTimeMillis();
int second = 5 * 1000;
Thread.sleep(second);
long end = System.currentTimeMillis();
System.out.println("Over Task 2, Run Time: " + (end-start) + "ms");
} catch (Exception e) {
System.out.println("Exit Task 2");
}
}
}

然后我们写一个用于启动上述任务的Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("runTask")
@RequiredArgsConstructor( onConstructor = @__(@Autowired) )
public class runTask {
private final SyncTask syncTask;

@RequestMapping("/sync")
public String runTask() {
System.out.println("Start runTask ...");
long start = System.currentTimeMillis();

syncTask.runTask1();
syncTask.runTask2();

long end = System.currentTimeMillis();
System.out.println("Over runTask, Run Time: " + (end-start) + "ms");
return "Sync Success";
}
}

启动使用Postman工具向runTask Controller发送请求,从下图执行结果可以看出,从输出和Postman给出的响应时间均可以看出整个Controller方法耗时15s左右,其主要耗时都是调用Task 1、Task 2中。而且可以看到这两个任务是同步调用顺序执行的

figure 1.jpeg

异步调用

配置异步线程池

在使用异步调用前,先编写一个异步线程池的配置类,其将会创建一个名为taskExecutor的TaskExecutor对象

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class TaskExecutorConfig {
@Bean
public TaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(50); // 任务队列容量
return executor;
}
}

无返回值的异步任务

对于线程1中无返回值的异步任务(task B、task C),通过将其放在新的线程(thread2、thread 3)中去完成,从而保证当前工作线程(thread 1)不会因上述任务执行时间过长而造成阻塞,异步调用示意图如下:

figure 2.jpeg

前文所述例子,两个任务执行结果对后续的方法执行不构成影响,且耗时过长效率低下,则可以通过异步的方式来并发执行。

这里给出一个异步调用的示例来详细说明,在Spring Boot 中提供了一个 @Async 注解,该注解将需要异步调用的方法会放在一个新线程下执行。我们对之前的任务类下的任务方法下进行一些适当的修改即可实现异步调用,首先需要进行异步调用的方法上添加 @Async 注解,然后在 SpringbootdemoApplication启动类上添加 @EnableAsync 注解来使能 @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
@Component
public class AsyncTask {
@Async("taskExecutor")
public void runTask1() {
try {
System.out.println("Start Task 1 ...");
long start = System.currentTimeMillis();
int second = 10 * 1000;
Thread.sleep(second);
long end = System.currentTimeMillis();
System.out.println("Over Task 1, Run Time: " + (end-start) + "ms");
} catch (Exception e) {
System.out.println("Exit Task 1");
}
}

@Async("taskExecutor")
public void runTask2() {
try {
System.out.println("Start Task 2 ...");
long start = System.currentTimeMillis();
int second = 5 * 1000;
Thread.sleep(second);
long end = System.currentTimeMillis();
System.out.println("Over Task 2, Run Time: " + (end-start) + "ms");
} catch (Exception e) {
System.out.println("Exit Task 2");
}
}
}

然后我们写一个用于启动上述异步任务的Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping("runTask")
@RequiredArgsConstructor( onConstructor = @__(@Autowired) )
public class runTask
{
private final AsyncTask asyncTask;

@RequestMapping("/Async")
public String runTaskByAsync() {
System.out.println("Start runTask ...");
long start = System.currentTimeMillis();

asyncTask.runTask1();
asyncTask.runTask2();

long end = System.currentTimeMillis();
System.out.println("Over runTask, Run Time: " + (end-start) + "ms");

return "Async Success";
}
}

使能 @EnableAsync 注解

1
2
3
4
5
6
7
8
@EnableAsync
@SpringBootApplication
public class SpringbootdemoApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SpringbootdemoApplication.class);
app.run(args);
}
}

使用Postman工具向runTaskByAsync Controller发送请求,从下图执行结果可以看出,从输出和Postman给出的响应时间均可以看出整个Controller方法仅仅耗费14ms就结束了,而Task1、Task2两个任务则由于采用异步调用并发执行,大大提高了执行效率

figure 3.jpeg

有返回值的异步任务

和无返回值的异步任务不同的是,有返回值的异步任务(task B、C、D)分别在新的线程执行时,主线程(thread 1)需要阻塞等待所有异步任务全部完成,然后通过Future对象来获取各异步任务执行的结果。其意义在于,传统的同步调用多个任务时,其耗时将是其
各任务所需耗时之和,而如果通过异步调用完成,其全部执行时间将只是其中耗时最长的任务所需的耗时。异步调用示意图如下:

figure 4.jpeg

这里给出一个有返回值的异步调用的示例来详细说明,在Spring Boot 中提供了一个 @Async 注解,该注解将需要异步调用的方法会放在一个新线程下执行。我们对之前的任务类下的任务方法下进行一些适当的修改即可实现异步调用,首先需要进行异步调用的方法上添加 @Async 注解,然后在 SpringbootdemoApplication启动类上添加 @EnableAsync 注解来使能 @Async 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 具有返回值的异步任务
*/
@Component
public class AsyncTask3 {

@Async("taskExecutor")
public Future<String> runTask(String taskName, Integer taskSecond) {
String result = null;
try{
System.out.println("Start: "+ taskName + " ...");
long start = System.currentTimeMillis();
Thread.sleep(taskSecond * 1000);
long end = System.currentTimeMillis();
result = taskName +", Run Time: " + (end - start)/1000 + " s";
System.out.println("Over: " + result);
} catch (Exception e) {
System.out.println( taskName +" have error");
}
return new AsyncResult<>(result);
}
}

然后我们写一个用于启动上述异步任务的Controller:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 带有返回值的异步任务测试
*/
@RequestMapping("/Async3")
@ResponseBody
public String runTaskAsync3() {

Future<String> future = null;
List<Future<String>> futureList = new ArrayList<>();

long start = System.currentTimeMillis();
// 创建异步任务
future = asyncTask3.runTask("task 1", 11);
futureList.add(future);

future = asyncTask3.runTask("task 2", 18);
futureList.add(future);

future = asyncTask3.runTask("task 3", 17);
futureList.add(future);

// 异步任务执行结果查询
if(futureList != null && futureList.size() != 0 ) {

// 阻塞等待所有异步任务完成
boolean isAllDone = false;
while(!isAllDone) {
isAllDone = true;
for(Future<String> futureElement : futureList) {
// 该异步任务未完成
if( !futureElement.isDone() ) {
isAllDone = false;
break;
}
}
}
long end = System.currentTimeMillis();
// 取出各异步任务执行结果
System.out.println("========= 异步任务执行结果 =========");
for(Future<String> futureElement : futureList) {
try {
String result = futureElement.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("========= 当前线程耗时 =========");
System.out.println( (end-start)/1000 + " s" );
}
return "Good Test";
}

使用Postman工具向Controller发送请求,从下图执行结果可以看出,我们分别创建了3个异步任务,而其主线程耗只有才18s

figure 5.jpeg

Future类的常用方法:

  • isDone(): 判断该异步任务是否执行结束。正常执行结束、发生异常结束、被取消均会返回true
  • V get(): 获取该异步任务的结果。如果该异步任务还未执行结束,则该调用线程会进入阻塞状态
  • V get(long timeout, TimeUnit unit):带超时限制的get(),等待超时之后,该方法会抛出TimeoutException,如果异步任务结束拿到结果时,且超时时间还没有到,其会返回结果

Note

  • @Async 注解不能应用于静态方法上,否则, @Async 注解会失效,该静态方法仍将使用同步调用
  • @Async 注解不能应用于本类的方法上,否则, @Async 注解会失效,本类的方法仍将使用同步调用。所以对于需要异步调用的方法不能放在本类中,应该抽取出来单独放在另一个类中
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝