一、简介
@Async是spring框架中十分好用的异步写法:
1,在方法上使用该@Async注解,申明该方法是一个异步任务;
2,在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;
3,使用此注解的方法的类对象,必须是spring管理下的bean对象;
4,要想使用异步任务,需要在主类上开启异步配置,即,配置上@EnableAsync注解;
@Async注解工作原理
就是在调用被注解的方法时,Spring会将该方法的执行转移到线程池中的一个线程进行处理。执行完成后,方法的返回值将通过Future或CompletableFuture进行封装,以便获取方法的返回结果。
@Async 注解适用于以下场景,并具有以下优势:
- 网络请求:在处理网络请求时,可以使用 @Async 注解将请求发送和响应处理分离,提高系统的并发处理能力。
- 耗时计算:对于需要耗费大量时间的计算任务,可以使用 @Async 注解将计算过程放在后台执行,避免阻塞主线程,提高系统的响应速度。
- 并行处理:通过 @Async 注解,可以同时执行多个任务,将多个相互独立的任务并行处理,从而减少整体处理时间。
- 响应能力提升:使用异步编程可以避免阻塞主线程,提高系统的并发能力和响应能力,增强用户体验。
- 代码简化:使用 @Async 注解可以简化编程模型,将异步执行的逻辑与业务逻辑分离,使代码更清晰、易于维护。
异步执行通过将任务分解为多个并发执行的子任务,可以充分利用系统资源,提高系统的吞吐量和并发处理能力,从而提升系统的性能和响应能力。@Async 注解简化了异步编程的实现,使开发人员能够更方便地使用异步处理机制。同时,它还可以使代码更易于阅读和维护,提高开发效率。
二、 同步与异步理解
1、**同步:**同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果
2、异步:异步调用则是只是发送调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。
**例如:**在某个调用中,需要顺序调用 A, B, C三个过程方法;如他们都是同步调用,则需要将他们都顺序执行完毕之后,方算作过程执行完毕; 如B为一个异步的调用方法,则在执行完A之后,调用B,并不等待B完成,而是执行开始调用C,待C执行完毕之后,就意味着这个过程执行完毕了。在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,通过主线程和不同的业务子线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行而不会产生停滞等待的情况。
1、同步调用
下面通过一个简单示例来直观的理解什么是同步调用:
1、定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内)
@Component
public class Task {
public static Random random =new Random();
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
}
public void doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
}
public void doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
2、在单元测试用例中,注入Task对象,并在测试用例中执行doTaskOne、doTaskTwo、doTaskThree三个函数。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}
}
3、执行单元测试,可以看到类似如下输出:
开始做任务一
完成任务一,耗时:4256毫秒
开始做任务二
完成任务二,耗时:4957毫秒
开始做任务三
完成任务三,耗时:7173毫秒
任务一、任务二、任务三顺序的执行完了,换言之doTaskOne、doTaskTwo、doTaskThree三个函数顺序的执行完成。
2、 异步调用
上述的同步调用虽然顺利的执行完了三个任务,但是可以看到执行时间比较长,若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差,可以考虑通过异步调用的方式来并发执行。
在Spring Boot中,我们只需要通过使用**@Async注解**就能简单的将原来的同步函数变为异步函数,Task类改在为如下模式:
public static Random random = new Random();
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("开始做任务1");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务1,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务1完成");
}
@Async
public Future<String> doTaskTwo() throws Exception {
System.out.println("开始做任务2");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务2,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务2完成");
}
@Async
public Future<String> doTaskThree() throws Exception {
System.out.println("开始做任务3");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务3,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务3完成");
}
看看我们做了哪些改变:
1、在测试用例一开始记录开始时间
2、在调用三个异步函数的时候,返回Future类型的结果对象
3、在调用完三个异步函数之后,开启一个循环,根据返回的Future对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断
开始做任务一
开始做任务二
开始做任务三
完成任务三,耗时:37毫秒
完成任务二,耗时:3661毫秒
完成任务一,耗时:7149毫秒
任务全部完成,总耗时:8025毫秒
可以看到,通过异步调用,让任务一、二、三并发执行,有效的减少了程序的总运行时间
三、@Async的使用
1、使用@Async注解时要先在启动类上添加@EnableAsync注解
// 基于Java配置的启用方式:
@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }
// Spring boot启用:
@EnableAsync
@EnableTransactionManagement
public class SettlementApplication {
public static void main(String[] args) {
SpringApplication.run(SettlementApplication.class, args);
}
}
2、添加@Async注解
1)无返回值的调用方法
/**
* 带参数的异步调用 异步方法可以传入参数
* 对于返回值是void,异常会被AsyncUncaughtExceptionHandler处理掉
* @param s
*/
@Async
public void asyncInvokeWithException(String s) {
log.info("asyncInvokeWithParameter, parementer={}", s);
throw new IllegalArgumentException(s);
}
2)Future返回值
@Async("MyExecutor")
public Future<Map<Long, List>> queryMap(List ids) {
List<> result = businessService.queryMap(ids);
..............
Map<Long, List> resultMap = Maps.newHashMap();
...
return new AsyncResult<>(resultMap);
}
四、@Async的线程池问题
1、Spring默认线程池
1、@Async注解在使用时,如果不指定线程池的名称,则使用spring默认的线程池,spring默认的线程池为SimpleAsyncTaskExecutor
2、方法上一旦标记了这个@Async注解,当其它线程调用这个方法时,就会开启一个新的子线程去异步处理该业务逻辑。
spring的默认线程池配置为
默认核心线程数:8
最大线程数:Integet.MAX_VALUE
队列使用LinkedBlockingQueue
容量是:Integet.MAX_VALUE
空闲线程保留时间:60s
线程池拒绝策略:AbortPolicy
可以看到,他对最大线程数没有限制,也就是会无限多的创建线程
2、自定义线程池
1、编写配置文件
spring:
task:
execution:
pool:
max-size: 6
core-size: 3
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: name
2、编写配置类
@Configuration
@Data
public class ExecutorConfig{
/**
* 核心线程
*/
private int corePoolSize;
/**
* 最大线程
*/
private int maxPoolSize;
/**
* 队列容量
*/
private int queueCapacity;
/**
* 保持时间
*/
private int keepAliveSeconds;
/**
* 名称前缀
*/
private String preFix;
@Bean("MyExecutor")
public Executor myExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix(preFix);
executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
3、使用@Async注解并指定自定义线程池
@Component
public class MyAsyncTask {
@Async("MyExecutor") //使用自定义的线程池(执行器)
public void asyncCpsItemImportTask(Long platformId, String jsonList){
//...具体业务逻辑
}
}
五、@Async的事务与异常处理
1.由于@Async注解由于是异步执行的,在其进行数据库的操作之时,外层的事务控制将无法进行事务管理。可以把@Transactional注解放到内部的需要进行事务的方法上,来实现事务管理。
2.同样由于异步的关系,@Async方法中的抛出的异常无法被外部的try…catch捕获,因此要把try…catch写到异步方法内