• SpringBoot使用@Async的总结!


    一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。

    之前有写过一篇文章叫做: 异步编程利器:CompletableFuture

    在实际工作中也更加推荐使用CompletableFuture,因为它实现异步方式更加优雅,而且功能更加强大!

    既然SpringBoot能通过 @Async 也实现异步执行任务,那么这篇文章就来总结下如何使用 @Async 实现异步执行任务。

    一、SpringBoot使用@Async注解步骤

    1、启动类上使用@EnableAsync注解

    @SpringBootApplication
    @EnableAsync
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    2、异步方法所在的类注入容器中

     @Componet
     public class Test{
    
    }
    

    除了@Componet,也可以是@Controller、@RestController、@Service、@Configuration等注解,加入到Ioc容器里。

    3、方法上加上@Async注解

    @Service
     public class Test{
    
    	@Async
    	public void a() {
    
    	}
    }
    

    二、哪些情况会导致@Async异步失效?

    如果你明明是按照上面的步骤来的,但是发现@Async注解还是不起作用,这里还有两点注意,因为@Async是基于Aop思想实现的,所有下面两种情况也会失效。

    1、异步方法使用static修饰

        @Async
        public static void a() {
    
        }
    

    2、调用方法和异步方法在同一个类中

    当异步方法和调用方法在同一个类中时,是没办法通过Ioc里的bean来执行异步方法的,从而变成同步方法。

    如下:

    @Component
    public class Task {
    
        /**
         * 调异步方法和异步方法在同一个类 @Async执行失败
         */
        public void dotask() {
            this.taskOne();
            this.taskTwo();
        }
    
        @Async
        public void taskOne() {
            //执行任务1
        }
        
        @Async
        public void taskTwo() {
            //执行任务2
        }
    }
    

    三、SpringBoot结合@Async实现异步示例

    首先我们来看同步方法

    1、同步调用示例

    @Component
    @Slf4j
    public class DemoTask {
        
        public void taskOne() throws Exception {
            log.info("===执行任务1===");
            long start = System.currentTimeMillis();
            Thread.sleep(200);
            long end = System.currentTimeMillis();
            log.info("任务1执行结束,总耗时={} 毫秒", end - start);
        }
    
        public void taskTwo() throws Exception {
            log.info("===执行任务2===");
            long start = System.currentTimeMillis();
            Thread.sleep(200);
            long end = System.currentTimeMillis();
            log.info("任务2执行结束,总耗时={} 毫秒", end - start);
        }
    
        public void taskThere() throws Exception {
            log.info("===执行任务3===");
            long start = System.currentTimeMillis();
            Thread.sleep(200);
            long end = System.currentTimeMillis();
            log.info("任务3执行结束,总耗时={} 毫秒", end - start);
        }
    }
    
    

    执行方法

    @Slf4j
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class DemoTaskTest {
    
        @Autowired
        private DemoTask demoTask;
        
        @Test
        public void runDemo() throws Exception {
            long start = System.currentTimeMillis();
            demoTask.taskOne();
            demoTask.taskTwo();
            demoTask.taskThere();
            long end = System.currentTimeMillis();
            log.info("总任务执行结束,总耗时={} 毫秒", end - start);
        }
    }
    

    输出日志

    ===执行任务1===
    任务1执行结束,总耗时=204 毫秒
    ===执行任务2===
    任务2执行结束,总耗时=203 毫秒
    ===执行任务3===
    任务3执行结束,总耗时=201 毫秒
    总任务执行结束,总耗时=613 毫秒
    

    2、异步调用示例

    异步方法

    @Component
    @Slf4j
    public class AsyncTask {
    
        @Async
        public void taskOne() throws Exception {
          //执行内容同上,省略
        }
    
        @Async
        public void taskTwo() throws Exception {
          //执行内容同上,省略
        }
    
        @Async
        public void taskThere() throws Exception {
           //执行内容同上,省略
        }
    }
    

    调用方法

    @Slf4j
    @RunWith(SpringJUnit4ClassRunner.class)
    @EnableAsync
    @SpringBootTest
    public class AsyncTest {
    
        @Autowired
        private AsyncTask asyncTask;
    
        @Test
        public void runAsync() throws Exception {
            long start = System.currentTimeMillis();
            asyncTask.taskOne();
            asyncTask.taskTwo();
            asyncTask.taskThere();
            Thread.sleep(200);
            long end = System.currentTimeMillis();
            log.info("总任务执行结束,总耗时={} 毫秒", end - start);
        }
    }    
    

    查看日志

    ===执行任务1===
    ===执行任务3===
    ===执行任务2===
    总任务执行结束,总耗时=206 毫秒
    任务1执行结束,总耗时=200 毫秒
    任务2执行结束,总耗时=201 毫秒
    任务3执行结束,总耗时=201 毫秒
    

    通过日志可以看出已经是已经实现异步处理任务了,而且异步任务哪个先执行是不确定的。

    3、Future异步回调

    如果我想异步执行,同时想获取所有异步执行的结果,那么这个时候就需要采用Future。

    异步方法

    @Component
    @Slf4j
    public class FutureTask {
        
        @Async
        public Future taskOne() throws Exception {
            //执行内容同上,省略
            return new AsyncResult<>("1完成");
        }
        @Async
        public Future taskTwo() throws Exception {
            //执行内容同上,省略
            return new AsyncResult<>("2完成");
        }
    
        @Async
        public Future taskThere() throws Exception {
            //执行内容同上,省略
            return new AsyncResult<>("执行任务3完成");
        }
    }
    

    调用方法

    @Slf4j
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @EnableAsync
    public class FutureTaskTest {
    
        @Autowired
        private FutureTask futureTask;
    
        @Test
        public void runAsync() throws Exception {
            long start = System.currentTimeMillis();
            Future taskOne = futureTask.taskOne();
            Future taskTwo = futureTask.taskTwo();
            Future taskThere = futureTask.taskThere();
    
            while (true) {
                if (taskOne.isDone() && taskTwo.isDone() && taskThere.isDone()) {
                    log.info("任务1返回结果={},任务2返回结果={},任务3返回结果={},", taskOne.get(), taskTwo.get(), taskThere.get());
                    break;
                }
            }
            long end = System.currentTimeMillis();
            log.info("总任务执行结束,总耗时={} 毫秒", end - start);
        }
    }
    

    查看日志

    ===执行任务2===
    ===执行任务3===
    ===执行任务1===
    任务1执行结束,总耗时=201 毫秒
    任务3执行结束,总耗时=201 毫秒
    任务2执行结束,总耗时=201 毫秒
    任务1返回结果=1完成,任务2返回结果=2完成,任务3返回结果=执行任务3完成,
    总任务执行结束,总耗时=223 毫秒
    

    从日志可以看出 异步任务的执行结果都有获取。


    四、@Async+自定义线程池实现异步任务

    如果不自定义异步方法的线程池默认使用SimpleAsyncTaskExecutor线程池。

    SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。并发大的时候会产生严重的性能问题。

    Spring也更加推荐我们开发者使用ThreadPoolTaskExecutor类来创建线程池。

    自定义线程池

    @Configuration
    public class ExecutorAsyncConfig {
        
        @Bean(name = "newAsyncExecutor")
        public Executor newAsync() {
            ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
            //设置核心线程数
            taskExecutor.setCorePoolSize(10);
            // 线程池维护线程的最大数量,只有在缓冲队列满了以后才会申请超过核心线程数的线程
            taskExecutor.setMaxPoolSize(100);
            //缓存队列
            taskExecutor.setQueueCapacity(50);
            //允许的空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
            taskExecutor.setKeepAliveSeconds(200);
            //异步方法内部线程名称
            taskExecutor.setThreadNamePrefix("my-xiaoxiao-AsyncExecutor-");
            //拒绝策略
            taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            taskExecutor.initialize();
            return taskExecutor;
        }
    }
    

    示例代码

    任务1和任务2配置走我们自定义的线程池,任务3还是走默认线程池。

    @Component
    @Slf4j
    public class FutureExecutorTask {
    
        @Async("newAsyncExecutor")
        public Future taskOne() throws Exception {
            log.info("任务1线程名称 = {}", Thread.currentThread().getName());
            return new AsyncResult<>("1完成");
        }
        @Async("newAsyncExecutor")
        public Future taskTwo() throws Exception {
            log.info("任务2线程名称 = {}", Thread.currentThread().getName());
            return new AsyncResult<>("2完成");
        }
    
        @Async
        public Future taskThere() throws Exception {
            log.info("任务3线程名称 = {}", Thread.currentThread().getName());
            return new AsyncResult<>("执行任务3完成");
        }
    }
    

    调研方法和上面一样,我们再来看下日志

    任务2线程名称 = my-xiaoxiao-AsyncExecutor-2
    任务1线程名称 = my-xiaoxiao-AsyncExecutor-1
    任务3线程名称 = SimpleAsyncTaskExecutor-1
    总任务执行结束,总耗时=15 毫秒
    

    通过日志我们可以看出 任务1和任务2走的是我们自定义的线程池,任务3还是走默认线程池。


    五、CompletableFuture实现异步任务

    推荐这种方式来实现异步,它不需要在启动类上加@EnableAsync注解,也不需要在方法上加@Async注解,它实现更加优雅,而且CompletableFuture功能更加强大。

    具体可以看下之前写的文章:异步编程利器:CompletableFuture

    1、CompletableFuture示例

    看如何使用

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest
    public class CompletableTest {
    
        @Autowired
        private DemoTask dmoTask;
        
        @Test
        public void testCompletableThenRunAsync() throws Exception {
            long startTime = System.currentTimeMillis();
    
            CompletableFuture cp1 = CompletableFuture.runAsync(() -> {
                //任务1
                dmoTask.taskOne();
            });
            CompletableFuture cp2 = CompletableFuture.runAsync(() -> {
                //任务2
                dmoTask.taskTwo();
            });
            CompletableFuture cp3 = CompletableFuture.runAsync(() -> {
                //任务3
                dmoTask.taskThere();
            });
            
            cp1.get();
            cp2.get();
            cp3.get();
            //模拟主程序耗时时间
            System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
        }
    }
    

    查看日志

    ===执行任务1===
    ===执行任务2===
    ===执行任务3===
    任务3执行结束,总耗时=204 毫秒
    任务2执行结束,总耗时=203 毫秒
    任务1执行结束,总耗时=204 毫秒
    总共用时226ms
    

    从日志可以看出,通过CompletableFuture同样可以实现异步执行任务!



    声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!

  • 相关阅读:
    使用跳板机实现外网访问局域网内虚拟机的大数据及K8S集群【借助向日葵】
    【监控指标】监控系统-prometheus、grafana。容器化部署。go语言 gin框架、gRPC框架的集成
    Vue(贰)
    在JAVA中intValue()、parseInt()、valueOf()的区别
    【经验分享】突然我的SM.MS的图床没法访问了(内附解决方法)
    car包recode函数多分类变量的重新编码
    最佳实践:REST API 的 HTTP 请求参数
    React中render Props模式
    module.exports和exports,应该用哪个
    CentOS修改主机名
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/16671089.html