本文由 简悦 SimpRead 转码, 原文地址 www.cnblogs.com
一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。
之前有写过一篇文章叫做: 异步编程利器: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 同样可以实现异步执行任务!