为了快速的将同步方法变为异步方法,Spring提供了@Async@EnableAsync注解。两个注解搭配,就可以快速实现一个异步方法。

1 初步体验@Async注解

下面,我们假设有一个异步记录日志的方法,需要异步输出日志,代码如下。

定义异步日志输出类,需要是一个spring bean。

// 使用@Component,定义为一个spring bean。
// @EnableAsync开启spring异步执行能力
@Component
@EnableAsync
public class AsyncLogPrinter {
    // @Async注解可以用在方法上,表示该方法将会异步执行
    @Async
    public void log(String msg) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " log msg:" + msg);
    }
}

定义spring bean配置文件appcontext-core.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

    <context:component-scan base-package="com.chris" />
    <context:annotation-config />
</beans>

定义main入口类

public class AsyncDemo {
    public static void main(String[] args) {
        // 由于在main方法里面,因此需要获取context,然后从context获取spring bean。
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"appcontext-core.xml"});
        // 从spring context获取bean
        AsyncLogPrinter asyncLogPrinter = context.getBean("asyncLogPrinter", AsyncLogPrinter.class);
        long start = System.currentTimeMillis();
        // 该方法会异步执行,不会阻塞main线程
        asyncLogPrinter.log("hello");
        System.out.println(System.currentTimeMillis() - start);
    }
}

一种输出为

13
task-1 log msg:hello

从上面的例子可以看出,一个异步执行的方法通过两个注解就可以配置完成。图中例子,异步方法没有返回值,如果想要有返回值,Spring支持返回一个Future或者CompletableFuture。

2 @Async注解实现原理简介

从大致原理上来讲,spring容器启动时,会识别含有@EnableAsync注解和@Async注解的类;若发现某个方法上含有@Async注解,则为该方法所在的类创建AOP代理类,为该方法创建代理方法,代理方法将原方法包装为一个异步的方法。在运行时,代理方法将任务提交到线程池执行,并立即返回。
这只是大致的思路,我们会好奇,异步任务是在哪个线程池执行的?

ThreadPoolTaskExecutor

ThreadPoolTaskExecutor的类图大致如下(图中去掉了不妨碍理解ThreadPoolTaskExecutor的其他的类)

  • TaskExecutor
    该接口继承了java.util.concurrent.Executor(jdk Executor),其只有一个方法@Override void execute(Runnable task);。该接口的目的是将任务的提交和执行解耦,将任务的执行抽象出来,也就是说,用户提交任务只需要调用该接口就可以了,而任务的执行方式为异步、同步、使用线程池或者不使用线程池都可以。
    至于为什么要继承jdk Executor,而不是直接使用jdk Executor呢?原因有两个:
    • 若直接使用jdk Executor,则会受到jdk Executor接口演变的影响,没有做到解耦合。
    • 想要复用jdk Executor类已有的功能,则继承了jdk Executor。
  • SyncTaskExecutor
    TaskExecutor的一个实现类,会在调用线程同步执行任务。由于在同一个线程中执行任务,因此线程上下文相同,主要用于测试场景。建议选择异步的执行器。
  • AsyncTaskExecutor
    接口类,TaskExecutor的一个实现类,一个异步执行器。其方法如下:5c8e7a404cf8170a3afc03d8be6e248e.png除了继承自TaskExecutor的方法void execute(Runnable task);,AsyncTaskExecutor还额外提供了三个方法,其中execute(Runnable task, long startTimeout);的形参含有超时时间。
  • SchedulingTaskExecutor
    接口类,继承了AsyncTaskExecutor,只有一个方法boolean prefersShortLivedTasks(),表明了执行器是否希望提交的任务执行时间短。用户使用该类型的执行器时,能够知道该执行器的特点,从而提交适合该执行器的任务。
  • AsyncListenableTaskExecutor
    接口类,继承了AsyncTaskExecutor,扩展了AsyncTaskExecutor的能力,增加了返回ListenableFuture的能力。ListenableFuture继承了Future,可以接收已经完成了的Callback。当执行器添加Callback任务时,若发现Callback任务已经执行完成,则会立即触发Callback的回调函数。
  • SimpleAsyncTaskExecutor
    实现类,实现了AsyncListenableTaskExecutor。对于每个任务,该执行器都会新建一个线程,来异步的执行任务。默认情况下不限制新建的线程个数,但可以通过属性concurrencyLimit来设置。由于对于每个任务都新建线程来执行任务,因此,不建议使用该执行器。
  • ConcurrentTaskExecutor
    实现类,是对于java.util.concurrent.Executor执行器的一个包装,也就是说,该执行器可以认为是jdk Executor执行器。默认的构造器会使用Executors.newSingleThreadExecutor()来创建一个执行器,当然用户也可以指定一个Executor执行器,体现出了该执行器的灵活性。对于一般的场景,可以使用spring已有的ThreadPoolTaskExecutor来代替该执行器。
  • ThreadPoolTaskExecutor 实现类,将ThreadPoolExecutor包装为一个spring bean。可以通过设置该bean的属性corePoolSizemaxPoolSizekeepAliveSecondsqueueCapacity来控制执行器的特性,并且可以通过JMX来管理和监控执行器的特性。此外,可以在动态运行时修改参数corePoolSizemaxPoolSizekeepAliveSeconds。 ThreadPoolTaskExecutor的默认配置为:corePoolSize=1,maxPoolSize不限制,queueCapacity不限制。

@EnableAsync注解和@Async注解

我们这里只是从大致原理讲述一下spring支持异步方法的实现原理:

  1. spring在启动时,会识别注解@EnableAsync和@Async,并为含有@EnableAsync注解的bean创建代理bean。默认的代理为Cglib代理,具体的拦截器为AnnotationAsyncExecutionInterceptor
  2. 在运行时,实际运行的方法为代理bean的方法。

3 总结

本书讲述了spring框架的异步方法之怎么使用的,简述了spring怎么通过注解和动态代理来方便的支持异步方法的,并较详细的讲述了各个spring执行器的特点。