java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > @Async注解使用及注解失效解决

@Async注解的使用以及注解失效问题的解决

作者:豆腐脑lr

在Spring框架中,@Async注解用于声明异步任务,可以修饰类或方法,使用@Async时,必须确保方法为public,且类为Spring管理的Bean,启用异步任务需要在主类上添加@EnableAsync注解,默认线程池为SimpleAsyncTaskExecutor

1. @Async作用范围

@Async的注解如下,可以看出该注解可以修饰方法

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {
    String value() default "";
}

该注解使用要满足以下基本要求:

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

测试结果如下,可以看到确实开启了一个异步任务。

2.4 隐藏问题:默认线程池配置不合适,导致系统奔溃

@Async注解在使用时,如果不指定线程池的名称,则使用Spring默认的线程池,Spring默认的线程池为SimpleAsyncTaskExecutor。

该类型线程池的默认配置:

解决方法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注入会导致循环依赖。解决这个问题有几种方法:

  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()
   }
}
  1. 使用构造函数注入:将依赖通过构造函数进行注入而不是字段注入。这样可以避免循环依赖,因为在构造对象时就能明确传递依赖关系。
@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()
   }
}

至于用哪种方法,可以根据实际需求选择适合你场景的解决方案。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文