SpringBoot中的@Asnyc注解

前言

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

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

Controller线程安全

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

@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的线程池中拿到一个线程进行使用。

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
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@RestController
public class HelloController {
    
}

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

i = 1
threadId = 40
i = 1
threadId = 43
i = 1
threadId = 42
i = 1
threadId = 40

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

@Async异步调用

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

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

@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注解,开启异步功能

@EnableAsync
@SpringBootApplication
public class SpringBootSourceApplication {

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

    }
}

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

@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默认线程池的参数

task:
    execution:
      pool:
        core-size: 5
        max-size: 50
        queue-capacity: 200
      thread-name-prefix: myTask-
// 代码有删改,具体配置类
@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,默认线程池已经没了)

@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接口

@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属性,为不同任务指定不同的线程池

// 不同方法,指定不同的线程池

@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());
}
@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原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值