@Async注解的使用以及注解失效问题的解决
作者:豆腐脑lr
1. @Async作用范围
@Async
的注解如下,可以看出该注解可以修饰类
和方法
。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Async { String value() default ""; }
该注解使用要满足以下基本要求:
- 1)在方法上使用该@Async注解,申明该方法是一个异步任务;(必须是public的方法,不能是private的方法,否则注解会失效!!)
- 2)在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;
- 3)方法上一旦标记了这个@Async注解,当其它线程调用这个方法时,就会开启一个新的子线程去异步处理该业务逻辑。
- 4)使用此注解的方法的类对象,必须是spring管理下的bean对象 (如被@Service、@Component等修饰的Bean对象)
- 5)要想使用异步任务,需要在主类上开启异步配置,即配置上@EnableAsync注解
2. 基本使用方法
2.1 开启异步注解@EnableAsync
在SpringBoot的启动类上开启异步任务注解
@SpringBootApplication @EnableAsync public class AsyncDemoApplication { public static void main(String[] args) { SpringApplication.run(AsyncDemoApplication.class, args); } }
2.2 创建Bean对象及异步方法
@Component public class Aservice { @Async public void MethodA() { System.out.println("当前线程为:" + Thread.currentThread().getName()); } }
2.3 在Test方法中进行测试
@SpringBootTest class AsyncDemoApplicationTests { @Autowired private Aservice aservice; @Test void contextLoads() { System.out.println("当前线程名称:" + Thread.currentThread().getName()); aservice.MethodA(); } }
测试结果如下,可以看到确实开启了一个异步任务。
- 当前线程名称:main
- 当前线程为:task-1
2.4 隐藏问题:默认线程池配置不合适,导致系统奔溃
@Async注解在使用时,如果不指定线程池的名称,则使用Spring默认的线程池,Spring默认的线程池为SimpleAsyncTaskExecutor。
该类型线程池的默认配置:
- 默认核心线程数:8,
- 最大线程数:Integet.MAX_VALUE,
- 队列使用LinkedBlockingQueue,
- 容量是:Integet.MAX_VALUE,
- 空闲线程保留时间:60s,
- 线程池拒绝策略:AbortPolicy。
解决方法1: 修改配置文件,指定线程池参数
通过修改SpringBoot的配置文件application.yml
来解决上述问题:
spring: task: execution: thread-name-prefix: MyTask pool: max-size: 6 core-size: 3 keep-alive: 30s queue-capacity: 500
解决方法2:编写配置类
首先在application.yml
文件中自定义一些键值对。
mytask: execution: thread-name-prefix: myThread pool: max-size: 6 core-size: 3 keep-alive: 30 queue-capacity: 500
然后编写一个集成了AsyncConfig
的配置类
// 如果没有在启动类上加注解,在异步任务配置类中加也是可以的 @EnableAsync @Configuration public class AsyncExecutorConfig implements AsyncConfigurer { @Value(value="${mytask.execution.pool.core-size}") private String CORE_SIZE; @Value(value="${mytask.execution.pool.max-size}") private String MAX_SIZE; @Value("${mytask.execution.pool.queue-capacity}") private String QUEUE_SIZE; @Value("${mytask.execution.thread-name-prefix}") private String THREAD_NAME_PREFIX; @Value("${mytask.execution.pool.keep-alive}") private int KEEP_ALIVE; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Integer.parseInt(CORE_SIZE)); executor.setMaxPoolSize(Integer.parseInt(MAX_SIZE)); executor.setQueueCapacity(Integer.parseInt(QUEUE_SIZE)); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setRejectedExecutionHandler( (runnable, threadPoolExecutor) -> { try { threadPoolExecutor.getQueue().put(runnable); } catch (InterruptedException e) { System.out.println("Thread pool receives InterruptedException: " + e); } }); executor.initialize(); return executor; } }
这样在启动上述任务,就会打印出修改后的线程名称。
3. 带返回值和不带返回值的异步任务
3.1 不带返回值的异步任务。
在AService.java
中新增异步方法:
@Async public void MethodB() { for (int i = 0; i < 5; i++) { // 模拟任务执行需要5秒 System.out.println("线程-" + Thread.currentThread().getName() + "-业务" + i + "执行中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
为了方便测试,编写一个Controller接口,来测试该方法。
@RestController public class TestController { @Autowired private Aservice aservice; @GetMapping("/test1") public String test1() { System.out.println(Thread.currentThread().getName() + "线程开始..."); long start = System.currentTimeMillis(); aservice.MethodB(); long end = System.currentTimeMillis(); return "一共耗时:" + (end -start) + "毫秒"; } }
在浏览器访问对应接口,发现仅用了几毫秒的时间,实际MethodB的执行时间为5秒,说明异步方法成功。
3.2 带返回结果的异步任务。
编写一个带返回结果的异步任务。
@Async public Future<Integer> methodC() { // 模拟业务 执行需要5秒 System.out.println("当前线程为:" + Thread.currentThread().getName()); Integer result = null; for (int i = 0; i < 5; i++) { // 模拟任务执行需要5秒 System.out.println("线程-" + Thread.currentThread().getName() + "-业务" + i + "执行中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } result = 1; // 5秒后得到处理后的数据 System.out.println("methodC 执行完毕"); return new AsyncResult<>(result); }
在控制层进行调用,为了验证异步的效果,在控制层也加入3秒中的sleep().
@GetMapping("/getResult") public Integer getResult() throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName() + "线程开始..."); long start = System.currentTimeMillis(); Future<Integer> future = aservice.methodC(); Thread.sleep(3000); Integer result = future.get(); long end = System.currentTimeMillis(); System.out.println("一共耗时:" + (end - start) + "毫秒"); return result; }
执行结果如下,可以看出,尽管主线程中加入了3秒的休眠,整个任务还是只用了5秒的异步任务处理时长,说明任务是在异步执行的。
http-nio-8086-exec-1线程开始...
当前线程为:myThread1
线程-myThread1-业务0执行中...
线程-myThread1-业务1执行中...
线程-myThread1-业务2执行中...
线程-myThread1-业务3执行中...
线程-myThread1-业务4执行中...
methodC 执行完毕
一共耗时:5053毫秒
有些教程上面可能会直接在开启异步任务的时候就进行get()了,这种方法虽然开启了额外的线程,但主方法其实也堵塞在get()这行代码了,相当于就还是同步方法了。
如下:
@GetMapping("/getResult1") public Integer getResult1() throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName() #43; "线程开始..."); long start = System.currentTimeMillis(); Integer result = aservice.methodC().get(); Thread.sleep(3000); long end = System.currentTimeMillis(); System.out.println("一共耗时:" + (end - start) + "毫秒"); return result; }
通过运行结果可以看出,一共耗时8秒,如果是异步任务,只需要5秒。
http-nio-8086-exec-1线程开始...
当前线程为:myThread1
线程-myThread1-业务0执行中...
线程-myThread1-业务1执行中...
线程-myThread1-业务2执行中...
线程-myThread1-业务3执行中...
线程-myThread1-业务4执行中...
methodC 执行完毕
一共耗时:8049毫秒
4. 注解失效的可能原因及解决方法
4.1 异步方法修饰符非public
对于异步任务,要使用public
修饰符
@Component public class Aservice { @Async public void MethodA() { System.out.println("当前线程为:" + Thread.currentThread().getName()); } }
4.2 未开启异步配置
需要在SpringBoot启动类上添加@EnableAsync
注解
@SpringBootApplication @EnableAsync//开启异步线程配置 public class AsyncDemoApplication { public static void main(String[] args) { SpringApplication.run(AsyncDemoApplication.class, args); } }
或者在Aysnc配置类上添加@EnableAsync
注解
// 如果没有在启动类上加注解,在异步任务配置类中加也是可以的 @EnableAsync @Configuration public class AsyncExecutorConfig implements AsyncConfigurer { @Value(value="${mytask.execution.pool.core-size}") private String CORE_SIZE; @Value(value="${mytask.execution.pool.max-size}") private String MAX_SIZE; @Value("${mytask.execution.pool.queue-capacity}") private String QUEUE_SIZE; @Value("${mytask.execution.thread-name-prefix}") private String THREAD_NAME_PREFIX; @Value("${mytask.execution.pool.keep-alive}") private int KEEP_ALIVE; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Integer.parseInt(CORE_SIZE)); executor.setMaxPoolSize(Integer.parseInt(MAX_SIZE)); executor.setQueueCapacity(Integer.parseInt(QUEUE_SIZE)); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setRejectedExecutionHandler( (runnable, threadPoolExecutor) -> { try { threadPoolExecutor.getQueue().put(runnable); } catch (InterruptedException e) { System.out.println("Thread pool receives InterruptedException: " + e); } }); executor.initialize(); return executor; } }
4.3 同一个类的普通方法调用异步方法
如果在一个类中,方法A被@Async修饰,而方法B没有被@Async修饰,并且方法B调用了方法A,那么会导致@Async修饰的方法A的注解失效。原因是,对于对于加了@Async的方法A是通过SpringAOP机制生成的代理类执行的,方法B是直接调用这个类的方法,因此通过B调用A,会使得A也被Spring当成普通方法直接调用,从而使得注解失效。
可以通过以下两种方式来确保@Async注解生效:
方法1: 将方法A的调用放在另外一个Bean上,并通过依赖注入的方式使用该Bean。
@Component public class MyClass { private final MyAsyncService myAsyncService; public MyClass(MyAsyncService myAsyncService) { this.myAsyncService = myAsyncService; } @Async public void A() { // 异步操作内容 } public void B() { myAsyncService.A(); } } @Service public class MyAsyncService { @Async public void A() { // 异步操作内容 } }
在上述示例中,MyClass类中的方法B调用了MyAsyncService类中的方法A。由于MyClass类和MyAsyncService类是不同的Bean,在MyClass中直接调用myAsnycService.A()时,会触发异步操作。
方法2. 在同一个类内部使用self-invocation的方式来调用被@Async修饰的方法。
@Service public class MyService { @Autowired private MyService self; @Async public void A() { // 异步操作内容 } public void B() { self.A(); // 使用self-invocation调用被@Async修饰的方法A() } }
在上述示例中,MyService类内部使用@Autowired将自身注入到了self变量中,在B()方法中通过self.A()来调用被@Async修饰的A()方法。这样可以绕过Spring代理机制,保证A()方法能够以异步方式执行。
无论采取哪种方式,都能确保被@Asnyc修饰的方法在调用时能够以异步方式执行,而非直接在当前线程执行
上述代码,其实存在一个问题,即:因为MyService
类中使用了自身的实例作为依赖。这种情况下,使用@Autowired
注入会导致循环依赖。解决这个问题有几种方法:
- 使用
@Lazy
注解:将依赖的注入方式改为懒加载模式,即在需要使用时才进行实例化。您可以将@Autowired
注解改为@Autowired @Lazy
,以解决循环依赖的问题。
@Service public class MyService { @Autowired @Lazy private MyService self; @Async public void A() { // 异步操作内容 } public void B() { self.A(); // 使用self-invocation调用被@Async修饰的方法A() } }
- 使用构造函数注入:将依赖通过构造函数进行注入而不是字段注入。这样可以避免循环依赖,因为在构造对象时就能明确传递依赖关系。
@Service public class MyService { private final MyService self; @Autowired public MyService(MyService self) { this.self = self; } @Async public void A() { // 异步操作内容 } public void B() { self.A(); // 使用self-invocation调用被@Async修饰的方法A() } }
至于用哪种方法,可以根据实际需求选择适合你场景的解决方案。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。