SpringBoot中的@Asnyc注解

布鸽不鸽 Lv4

前言

本文将探讨在SpringBoot中的线程问题。Controller是线程安全的吗?如果我们想在用户请求时,开辟新的异步任务,该如何操作?

原文地址:https://xuedongyun.cn/post/59240/

Controller线程安全

首先我们先测试一下Controller的线程问题。我们在Controller中创建成员变量,并在请求中对它进行更改(请注意,这是非常规操作,请勿在开发中使用)。并且,我们在收到请求时,打印当前线程的Id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class HelloController {
private int i = 0;

@PostMapping("/hello")
String hello() {
i = i + 1;
System.out.println("i = " + i);

long threadId = Thread.currentThread().getId();
System.out.println("threadId = " + threadId);

return "hello";
}
}

最终结果显示,Controller默认是单例模式,而这种模式下是线程不安全的。我们每次的请求,都会从SpringBoot的线程池中拿到一个线程进行使用。

1
2
3
4
5
6
7
8
i = 1
threadId = 40
i = 2
threadId = 43
i = 3
threadId = 42
i = 4
threadId = 40 // 线程池中,线程可以复用

如果我们将Controller指定为单例模式,又会如何呢?我们使用@Scope注解,指定HelloController为原型模式。

@Scope有五种作用域:

  • SINGLETON:单例模式,默认模式,不写的时候默认是SINGLETON
  • PROTOTYPE:原型模式
  • REQUEST:同一次请求则只创建一次实例
  • SESSION:同一个session只创建一次实例
  • GLOBAL SESSION:全局的web域,类似于servlet中的application
1
2
3
4
5
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@RestController
public class HelloController {

}

此时可以看到,每次请求都会创建一个新的Controller实例,所以其实是线程安全的。

1
2
3
4
5
6
7
8
i = 1
threadId = 40
i = 1
threadId = 43
i = 1
threadId = 42
i = 1
threadId = 40

无论如何,请尽量不要在Controller中使用成员变量

@Async异步调用

假设用户提交一个任务,后端需要处理很久,最佳的方案应该是使用异步调用。用户提交任务之后,后端开辟新的线程处理任务。

首先在需要异步执行的方法上加上@Async注解

1
2
3
4
5
6
7
8
9
10
11
@Component
@Slf4j
public class AsyncTask {

@Async
public void doTask(String taskName) throws InterruptedException {
Thread.sleep(3000);
log.info(Thread.currentThread().getName());
log.info("task: " + taskName+ " is finished!");
}
}

然后需要在主启动类上加上@EnableAsync注解,开启异步功能

1
2
3
4
5
6
7
8
9
@EnableAsync
@SpringBootApplication
public class SpringBootSourceApplication {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootSourceApplication.class, args);

}
}

现在我们就能对用户的请求进行异步的处理了,用户发起请求能直接收到响应,3000ms后服务器才完成任务

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class AsyncController {

@Resource
private AsyncTask asyncTask;

@GetMapping("/task")
private String task(String taskName) throws InterruptedException {
asyncTask.doTask(taskName);
return "success to submit";
}
}

自定义线程池

配置文件修改默认线程池

我们可以通过配置文件来修改SpringBoot默认线程池的参数

1
2
3
4
5
6
7
task:
execution:
pool:
core-size: 5
max-size: 50
queue-capacity: 200
thread-name-prefix: myTask-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 代码有删改,具体配置类
@ConfigurationProperties("spring.task.execution")
public class TaskExecutionProperties {

private final Pool pool = new Pool();
private final Shutdown shutdown = new Shutdown();
private String threadNamePrefix = "task-";

public static class Pool {
private int queueCapacity = Integer.MAX_VALUE;
private int coreSize = 8;
private int maxSize = Integer.MAX_VALUE;
}

// ...
}

配置类定义新的线程池

也可以在配置类中定义自己的线程池(由于@ConditionalOnMissingBean,默认线程池已经没了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class AsyncConfig {

@Bean(name = "customTaskExecutor")
public ThreadPoolTaskExecutor customTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix(executorPrefix);
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}

AsyncConfigurer接口

在配置类中实现AsyncConfigurer接口

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
@Configuration
public class AsyncConfig implements AsyncConfigurer{

// 指定默认线程池
@Override
public Executor getAsyncExecutor() {
return getExecutor();
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler();
}

public ThreadPoolTaskExecutor getExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("custom-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}

为任务指定不同线程池

@Async不指定具体的线程池,会使用默认的线程池。具体规则如下:

  • 若容器中只有一个TaskExecutor组件,其为默认执行器;

  • 若不唯一,拿名字叫”taskExecutor”的,类型为Executor的组件。

  • 若都不满足,使用SimpleAsyncTaskExecutor作为默认执行器(每次执行被注解方法时,单独创建一个Thread来执行)

我们可以通过value属性,为不同任务指定不同的线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不同方法,指定不同的线程池

@Async("otherTaskExecutor")
public void doTask1() throws InterruptedException {
Thread.sleep(3000);
log.info(Thread.currentThread().getName());
}

@Async("testTaskExecutor")
public void doTask2() throws InterruptedException {
Thread.sleep(3000);
log.info(Thread.currentThread().getName());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Bean(name = "otherTaskExecutor")
public ThreadPoolTaskExecutor otherExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("other-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}

@Bean(name = "testTaskExecutor")
public ThreadPoolTaskExecutor testExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("test-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}

原理

如果你想知道背后的原理(源码),可以查看我之前的文章:SpringBoot源码系列(10):@Async原理

  • 标题: SpringBoot中的@Asnyc注解
  • 作者: 布鸽不鸽
  • 创建于 : 2023-06-12 18:32:03
  • 更新于 : 2023-08-28 18:56:57
  • 链接: https://xuedongyun.cn//post/59240/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论