3

SpringBoot使用@Async的总结! - 雨点的名字

 1 year ago
source link: https://www.cnblogs.com/qdhxhz/p/16671089.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
1090617-20220908223527724-495345590.jpg

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

之前有写过一篇文章叫做: 异步编程利器: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原稿和原图片。其它情况一律禁止转载!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK