开发者

@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 同一个类的普通方法调用异步方法
  • 总结

    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.setKeeppythonAliveSeconds(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秒
                Syshttp://www.devze.comtem.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注解的使用以及注解失效问题的解决

    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.ephpxecution.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()
       }
    }

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

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜