本文转载,原文地址:【异步任务】@Async注解使用方法及注解失效解决办法
目录
1. @Async作用范围
2. 基本使用方法
2.1 开启异步注解@EnableAsync
2.2 创建Bean对象及异步方法
2.3 在Test方法中进行测试
2.4 隐藏问题:默认线程池配置不合适,导致系统奔溃
3. 带返回值和不带返回值的异步任务
3.1 不带返回值的异步任务。
3.2 带返回结果的异步任务。
4. 注解失效的可能原因及解决方法
4.1 异步方法修饰符非public
4.2 未开启异步配置
4.3 同一个类的普通方法调用异步方法
@Async
的注解如下,可以看出该注解可以修饰类
和方法
。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {
String value() default "";
}
该注解使用要满足以下基本要求:
在SpringBoot的启动类上开启异步任务注解
@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}
@Component
public class Aservice {
@Async
public void MethodA() {
System.out.println("当前线程为:" + Thread.currentThread().getName());
}
}
@SpringBootTest
class AsyncDemoApplicationTests {
@Autowired
private Aservice aservice;
@Test
void contextLoads() {
System.out.println("当前线程名称:" + Thread.currentThread().getName());
aservice.MethodA();
}
}
测试结果如下,可以看到确实开启了一个异步任务。
当前线程名称:main
当前线程为:task-1
@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;
}
}
这样在启动上述任务,就会打印出修改后的线程名称。
在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秒,说明异步方法成功。
编写一个带返回结果的异步任务。
@Async
public Future 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 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() + "线程开始...");
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毫秒
对于异步任务,要使用public
修饰符
@Component
public class Aservice {
@Async
public void MethodA() {
System.out.println("当前线程为:" + Thread.currentThread().getName());
}
}
需要在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;
}
}
如果在一个类中,方法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注入会导致循环依赖。解决这个问题有几种方法:
1、使用@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()
}
}
2、使用构造函数注入:将依赖通过构造函数进行注入而不是字段注入。这样可以避免循环依赖,因为在构造对象时就能明确传递依赖关系。
@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()
}
}
至于用哪种方法,可以根据实际需求选择适合你场景的解决方案。
1、 Spring中的异步方法@Async失效的原因有哪些,代码演示