SpringBoot使用@Async的总结!

《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<String> taskOne() throws Exception {
        //执行内容同上,省略
        return new AsyncResult<>("1完成");
    }
    @Async
    public Future<String> taskTwo() throws Exception {
        //执行内容同上,省略
        return new AsyncResult<>("2完成");
    }

    @Async
    public Future<String> 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<String> taskOne = futureTask.taskOne();
        Future<String> taskTwo = futureTask.taskTwo();
        Future<String> 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<String> taskOne() throws Exception {
        log.info("任务1线程名称 = {}", Thread.currentThread().getName());
        return new AsyncResult<>("1完成");
    }
    @Async("newAsyncExecutor")
    public Future<String> taskTwo() throws Exception {
        log.info("任务2线程名称 = {}", Thread.currentThread().getName());
        return new AsyncResult<>("2完成");
    }

    @Async
    public Future<String> 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<Void> cp1 = CompletableFuture.runAsync(() -> {
            //任务1
            dmoTask.taskOne();
        });
        CompletableFuture<Void> cp2 = CompletableFuture.runAsync(() -> {
            //任务2
            dmoTask.taskTwo();
        });
        CompletableFuture<Void> 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原稿和原图片。其它情况一律禁止转载!

点赞