《Java异步编程实战》读书笔记

145 阅读25分钟

第1章 认识异步编程

异步编程是可以让程序并行运行的一种手段,其可以让程序中的一个工作单元与主应用程序线程分开独立运行,并且在工作单元运行结束后,会通知主应用程序线程它的运行结果或者失败原因。
为了更好地处理异步编程,降低异步编程的成本,一些框架也应运而生,比如高性能线程间消息传递库Disruptor,其通过为事件(event)预先分配内存、无锁CAS算法、缓冲行填充、两阶段协议提交来实现多线程并发地处理不同的元素,从而实现高性能的异步处理。比如Akka基于Actor模式实现了天然支持分布式的使用消息进行异步处理的服务;比如高性能分布式消息中间件Rocket MQ实现了应用间的异步解耦、流量削峰。

第2章 显式使用线程和线程池实现异步编程

2.1 显式使用线程实现异步编程

有两种方式显式开启一个线程进行异步处理

  1. 实现Runnable接口的run方法。然后传递Runnable接口的实现类作为创建Thread时的参数
  2. 实现Thread类,重写run方法

Java中线程是有Deamon与非Deamon之分的,默认情况下,我们创建的线程都是非Deamon线程,线程是什么类型与JVM退出条件有一定的关系。 当JVM进程内不存在非Deamon的线程时JVM就退出了。

显式创建线程的问题

  1. 每次执行异步任务都会创建新的线程,可能导致系统线程用尽,且没办法复用已有线程,增加开销
  2. 没有返回值
  3. 每次执行任务,都需要显示创建线程并启动。这是典型的命令式编程,增加编程者的心智负担

2.2 显式使用线程池实现异步编程

2.2.1 如何显式使用线程池实现异步编程

public class SyncExample2 {
    private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    public static String doSomethingA() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        System.out.println("---doSomethingA---");
        return "A";
    }

    public static void doSomethingB() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        System.out.println("--- doSomethingB---");
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(AVALIABLE_PROCESSORS, AVALIABLE_PROCESSORS * 2, 1, TimeUnit.HOURS, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());
        long start = System.currentTimeMillis();
        //1.开启异步单元执行任务A
        threadPoolExecutor.execute(()->{
            try {
                doSomethingA();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        //2.执行任务B
        doSomethingB();

        //3.同步等待线程A运行结束
        System.out.println(System.currentTimeMillis() - start);
        //4.挂起当前线程
        Thread.currentThread().join();
    }
}

其实可以通过submit方法来获取方法的返回值

   Future<String> resultA = threadPoolExecutor.submit(() -> doSomethingA());  
System.out.println(resultA.get());

以上方法虽然可以获取到异步任务的执行结果,但是main函数所在线程会阻塞,在异步线程执行完毕前,main函数所在线程就不能做其他事情了。

2.2.2 线程池ThreadPoolExecutor原理剖析

1.概述

ThreadPoolExecutor.png

线程池状态含义:

  • RUNNING:接收新任务并且处理阻塞队列里的任务
  • SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务
  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时中断正在处理的任务
  • TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
  • TERMINATED:终止状态。terminated()方法调用完成以后的状态

线程池状态之间转化路径:

  • RUNNING -> SHUTDOWN:当显示调用shutdown()方法时,或者隐式调用了finalize(),它里面调用了shutdown()方法时。
  • RUNNING或者SHUTDOWN -> STOP:当显式调用shutdownNow()方法时
  • SHUTDOWN -> TIDYING:当线程池和任务队列都为空时
  • STOP -> TIDYING:当线程池为空时
  • TIDYING -> TERMINATED:当terminated()方法执行完成时

线程池同时还提供了一些方法来获取线程池的运行状态和线程池中的线程个数:

  1. runStateOf(int c): 获取运行状态
  2. workerCount(int c): 获取线程个数
  3. ctlOf(int rs,int wc):计算ctl新值,线程状态和线程个数

线程池配置参数:

  1. corePoolSize: 线程池核心线程个数
  2. workQueue: 用于保存等待执行的任务的阻塞队列。比如基于数组的有界ArrayBlockingQueue、基于链表的无界LinkedBlockingQueue、最多只有一个元素的同步队列SynchronousQueue、优先级队列PriorityBlockingQueue等。
  3. maximunPoolSize: 线程池的最大线程数量
  4. threadFactory: 创建线程的工厂类
  5. defaultHandler:饱和策略,当队列满了并且线程个数达到maximunPoolSize后采取的策略,比如AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)、DiscardPolicy(默默丢弃,不抛出异常)
  6. keepAliveTime: 存活时间。如果当前线程池中的线程数量比核心线程数量要多,并且是闲置状态的话,这些闲置的线程能存活的最大时间

第3章 基于JDK中的Future实现异步编程

第4章 Spring框架中的异步执行

4.3 @Async注解异步执行原理

AsynAnnotationExample.class

    @Component
    public class AsynAnnotationExample {
        @Async
        public CompletableFuture<String> doSomething() {
            System.out.println(this.getClass());
            //1.创建future
            CompletableFuture<String> result = new CompletableFuture<>();
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "doSomething");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            result.complete("done");
            //3.返回结果
            return result;
        }
    }

由于AsynAnnotationExample类中方法doSomething被标注了@Async注解,所以Spring框架会对AsynAnnotationExample的实例进行代理,代理后的类代码如下所示:

AsynAnnotationExampleProxy类

    public class AsynAnnotationExampleProxy {
        private AsynAnnotationExample asyncTask;
        private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();

        public void setAsyncTask(AsynAnnotationExample asyncTask) {
            this.asyncTask = asyncTask;
        }

        public AsynAnnotationExample getAsyncTask() {
            return asyncTask;
        }

        public CompletableFuture<String> doSomethingAsyncFuture() {
            return CompletableFuture.supplyAsync(() -> {
                try {
                    return asyncTask.doSomething().get();
                } catch (Throwable e) {
                    throw new CompletionException(e);
                }
            }, taskExecutor);
        }
    }

如上代码所示,Spring会对AsynAnnotationExample进行代理,并且会把AsyncAnotationExample的实例注入AsyncAnotationExampleProxy内部,当我们调用AsyncAnotationExample的doSomething()方法时,后者使用CompletableFuture.suppleAsync开启了一个异步任务(其马上返回一个CompletableFuture对象),并且使用默认的SimpleAsyncTaskExecutor线程池作为异步处理线程,然后在异步任务内具体使用了AsyncAnnotationExample实例的doSomething()方法。

默认情况下,Spring框架是使用了Cglib对标注@Async注解的方法进行代理的,具体拦截器是AnnotationAsyncExecutionInterceptor,看看invoke方法。

public Object invoke(final MethodInvocation invocation) throws Throwable {
    //1.被代理的对象
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
    //2.获取被代理的方法
    final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
    //3.判断使用哪个执行器执行被代理的方法
    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
    if (executor == null) {
        throw new IllegalStateException(
                "No executor specified and no default executor set on AsyncExecutionInterceptor either");
    }

    //4.使用Callable包装要执行的方法
    Callable<Object> task = () -> {
        try {
            Object result = invocation.proceed();
            if (result instanceof Future) {
                return ((Future<?>) result).get();
            }
        }
        catch (ExecutionException ex) {
            handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
        }
        catch (Throwable ex) {
            handleError(ex, userDeclaredMethod, invocation.getArguments());
        }
        return null;
    };
    //5.提交包装的Callable任务到指定执行器执行
    return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
  • 代码1获取被代理的目标对象的Class对象,本例中为AsynAnnotationExample的class对象‘
  • 代码2获取被代理的方法,本例为AsynAnnotationExample.doSomething()
  • 代码3根据规则获取使用哪个执行器TaskExecutor执行被代理的方法,其代码如下所示。
	private final Map<Method, AsyncTaskExecutor> executors = new ConcurrentHashMap<>(16);
	protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
		//4.1获取对应方法的执行器
		AsyncTaskExecutor executor = this.executors.get(method);
		//4.2不存在则按照规则查找
		if (executor == null) {
			//4.2.1如果注解@Async中指定了执行器名称
			Executor targetExecutor;
			String qualifier = getExecutorQualifier(method);
			if (StringUtils.hasLength(qualifier)) {
				targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
			}
			//4.2.2 获取默认执行器
			else {
				targetExecutor = this.defaultExecutor.get();
			}
			//4.2.3
			if (targetExecutor == null) {
				return null;
			}
			//4.2.4 添加执行器到缓存
			executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
					(AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
			this.executors.put(method, executor);
		}
		//4.3 返回查找的执行器
		return executor;
	}

代码4.1从缓存executors中尝试获取method方法对应的执行器,如果存在则直接执行代码4.3返回;否则执行代码4.2.1判断方法的注解@Async中是否指定了执行器名称,如果有则尝试从Spring的bean工厂内获取该名称的执行器的实例,否则执行代码4.2.2获取默认的执行器(SimpleAsyncTaskExecutor),然后代码4.2.4把执行器注入缓存。
到这里所有的执行使用的都是调用线程,调用线程提交异步任务到执行器后就返回了,异步任务真正执行的是具体执行器中的线程。下面来看看代码5 doSubmit的代码。

protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
	//5.1 判断方法返回值是否为CompletableFuture类型或者其子类
	//这里的isAssignableFrom是native修饰的
	if (CompletableFuture.class.isAssignableFrom(returnType)) {
		return CompletableFuture.supplyAsync(() -> {
			try {
				return task.call();
			}
			catch (Throwable ex) {
				throw new CompletionException(ex);
			}
		}, executor);
	}
	//5.2 判断返回值类型是否为Listenable类型或者其子类
	else if (ListenableFuture.class.isAssignableFrom(returnType)) {
		return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
	}
	//5.3 判断返回值类型是否为Future
	else if (Future.class.isAssignableFrom(returnType)) {
		return executor.submit(task);
	}
	//5.4 其他情况下没有返回值
	else {
		executor.submit(task);
		return null;
	}
}

要开始异步处理,必须使用@EnableAsync注解来开启异步处理。

添加@EnableAsync注解后发生什么?

在Spring容器启动的过程中会有一系列扩展接口对Bean的元数据定义、初始化、实例化做拦截处理,也存在一些处理器可以动态地向Spring容器添加一些框架需要的Bean实例。其中的ConfigurationClassPostProcessor处理器类则是用来解析注解类,并把其注册到Spring容器中,其可以解析标注@Configuration、@Component、@ComponentScan、@Import、@ImportResource等的Bean。

而@EnableAsync的定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
 ...
}

添加了@EnableAsyn注解后,ConfigurationClassPostProcessor会解析其中的@Import(AsyncConfigurationSelector.class),并把AsyncConfigurationSelector的实例注入到Spring容器。
AsyncConfigurationSelector的代码如下:

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

	private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
			"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";


	@Override
	@Nullable
	public String[] selectImports(AdviceMode adviceMode) {
		//Spring AOP的实现方式是proxy-based。AOP 默认使用JDK动态代理作为AOP Proxy的实现方式。JDK动态代理能够代理被代理对象的所有接口
		//AdviceMode就两个枚举值,其中PROXY是JDK提供的代理,ASPECTJ是AspectJ提供的AspectJ
		switch (adviceMode) {
			case PROXY:
				return new String[] {ProxyAsyncConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
			default:
				return null;
		}
	}

}

AsyncConfigurationSelector实现了InportSelector接口的selectImports方法,根据AdviceMode参数返回需要导入到Spring容器的Bean的全限定名(怎么导入进去的呢?)。 该方法会被ConfiguarationClassPostProcessor中的ConfigurationClassParser类中调用。默认情况下的adviceMode为PROXY,所以会把ProxyAsyncConfiguration的实例注入到Spring容器。

ProxyAsyncConfiguration的代码如下所示:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {

	@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
		Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
		AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
		bpp.configure(this.executor, this.exceptionHandler);
		Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
		if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
			bpp.setAsyncAnnotationType(customAsyncAnnotation);
		}
		bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
		bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
		return bpp;
	}

}

如上代码,ProxyAsyncConfiguration的asyncAdvisor方法添加了@Bean注解,所以该方法返回的Bean也会被注入Spring容器。该方法创建了AsyncAnnotationBeanPostProcessor处理器,所以AsyncAnnotationBeanPostProcessor的一个实例会被注入到Spring容器中,由于其实现了BeanFactoryAware接口,所以Spring框架会调用其setBeanFactory(BeanFactory beanFactory)方法把Spring BeanFactory(存放bean的容器)注入到该Bean。
AsyncAnnotationBeanPostProcess的setBeanFactory方法代码如下所示:

   @Override
	public void setBeanFactory(BeanFactory beanFactory) {
		super.setBeanFactory(beanFactory);

		AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
		if (this.asyncAnnotationType != null) {
			advisor.setAsyncAnnotationType(this.asyncAnnotationType);
		}
		advisor.setBeanFactory(beanFactory);
		this.advisor = advisor;
	}

如上代码创建了一个AsyncAnnotationAdvisor的实例并保存到了AsyncAnnotationBeanPostProcessor的advisor变量。Spring对每个AsyncAnnotationAdvisor都包含一个Advice(切面)和PoinCut(切点),也就是会对符合PoinCut的方法使用Advice进行功能增强,对应Advice和PointCut是在AsyncAnnotationAdvisor构造函数内创建的。
AsyncAnnotationAdvisor的构造方法:

public AsyncAnnotationAdvisor(
	@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
    //6.1 异步注解类型
Set<Class<? extends Annotation>> asyncAnnotationTypes = new LinkedHashSet<>(2);
asyncAnnotationTypes.add(Async.class);
try {
	asyncAnnotationTypes.add((Class<? extends Annotation>)
			ClassUtils.forName("javax.ejb.Asynchronous", AsyncAnnotationAdvisor.class.getClassLoader()));
}
catch (ClassNotFoundException ex) {
	// If EJB 3.1 API not present, simply ignore.
}
//6.2 创建切面逻辑
this.advice = buildAdvice(executor, exceptionHandler);
//6.3 创建切点
this.pointcut = buildPointcut(asyncAnnotationTypes);
}

如上,6.1收集注解@Async和@javax.ejb.Asynchronous到asyncAnnotationTypes
6.2则创建Advice

protected Advice buildAdvice(
	@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {

AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
interceptor.configure(executor, exceptionHandler);
return interceptor;
}

这里创建了AnnotationAsyncExecutionInterceptor拦截器作为切面。
6.3 创建切点

protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {
	ComposablePointcut result = null;
	for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
		Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
		Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
		if (result == null) {
			result = new ComposablePointcut(cpc);
		}
		else {
			result.union(cpc);
		}
		result = result.union(mpc);
	}
	return (result != null ? result : Pointcut.TRUE);
}

在上述代码中使用收集的注解集合asyncAnnotationTypes,并在每个注解处创建了下一个AnnotationMatchingPointcut作为切点,AnnotationMatchingType内部的AnnotationClassFilter的方法matches则作为某个方法是否满足切点的条件。具体代码如下:

   @Override
	public boolean matches(Class<?> clazz) {
		return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
				clazz.isAnnotationPresent(this.annotationType));
	}

判断方法是通过"是否有注解@Async为依据"来判断方法是否符合切点。

总结下AsyncAnnotationBeanPostProcessor的setBeanFactory(BeanFactory beanFactory):

AsyncAnnotationBeanPostProcessor内部保存一个AsyncAnnotationAdvisor对象来对Spring容器中符合条件的(这里为含有@Async注解的方法的Bean)的Bean的方法进行功能增强。

AsyncAnnotationBeanPostProcessor的postProcessAfterInitialization方法是如何对这些符合条件的Bean进行代理的?

   @Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		...
		if (isEligible(bean, beanName)) {
                   //7.1 
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			if (!proxyFactory.isProxyTargetClass()) {
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
                    //7.2 设置拦截器
			proxyFactory.addAdvisor(this.advisor);
			customizeProxyFactory(proxyFactory);

			// Use original ClassLoader if bean class not locally loaded in overriding class loader
			ClassLoader classLoader = getProxyClassLoader();
			if (classLoader instanceof SmartClassLoader && classLoader != bean.getClass().getClassLoader()) {
				classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
			}
                    //7.3 获取代理类
			return proxyFactory.getProxy(classLoader);
		}

		// No proxy needed.
		return bean;
	}
    

如上代码7.1使用prepareProxyFactory创建了代理工厂,其代码如下所示。

protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
	ProxyFactory proxyFactory = new ProxyFactory();
	proxyFactory.copyFrom(this);
	proxyFactory.setTarget(bean);
	return proxyFactory;
}

代码7.2则设置在其setBeanFactory方法内创建的AsyncAnnotationAdvosor对象作为Advisor,代码7.3从代理工厂获取代理后的Bean实例并返回到Spring容器,所以当我们调用含有@Async注解的Bean的方法时候,实际调用的是被代理后的Bean。
当我们调用被代理的类的方法时,代理类内部会先使用AsyncAnnotationAdvisor中的PointCut进行比较,看其是否符合切点条件(方法是否含有@Async)注解,如果不符合则直接调用被代理的对象的原生方法,否则调用AsyncAnnotationAdvisor内部的AnnotationAsyncExecutionInterceptor进行拦截异步处理。

第5章 基于反应式编程实现异步编程

5.1 反应式编程概述

反应式编程(Reative Programming) 是一种涉及数据流和变化传播的异步编程范式。这意味着可以通过所采用的编程语言轻松地表达静态(例如阵列)或动态(例如事件发射器)数据流。

例如在命令式编程中表达式a=b+c,之后即使b和c的值发生变化,对变量a的值也没啥影响。而在反应式编程中,变量a的值则会随着变量b和变量c的改变而自动改变。这和我们在Excel中使用加法公式类似,当我们修改参与计算的加数的值时,Excel会自动帮我们更新和。

根据反应式宣言所述,使用反应式编程构建的反应式系统具有如下特征。

  • 即时响应式(Responsive):只要有可能,系统就会及时地做出响应。即时响应是可用性和实现性的基石,并且及时响应意味着可以快速地检测到问题并且可以有效地对其进行处理。
  • 回弹性(Resilient):系统在面临失败时仍然保持即时响应性。失败被包含在每个组件中,将组件彼此进行隔离,从而确保系统的某些组件可以在不损害整个系统的情况下发生故障和进行恢复。
  • 弹性(Elatic):系统在不断变化的工作负载下仍然保持即时响应性。
  • 消息驱动(Message Driven):反应式系统依靠异步消息传递在组件之间建立边界,以确保松散耦合、隔离和位置透明性,该边界还提供将故障委派为消息投递出去的方法。

第6章 Web Servlet的异步非阻塞处理

本章主要探讨Servlet3.0规范前的同步处理模型和缺点,Serlvlet3.0规范提供的异步处理能力与Serlvet3.0规范提供的非阻塞IO能力,以及Spring MVC中提供的异步处理能力。

6.1 Serlvet概述

Servlet是一个给予Java技术的Web组件,由容器管理,生成动态内容。像其他基于Java技术的组件一样,Servlet是与平台无关的Java类格式,它们被编译为与具体平台无关的字节码,可以被给予Java技术的Web Server动态加载并运行。 容器(有时称为Servlet引擎)是Web服务器为支持Servlet功能扩展的部分。客户端通过Servlet容器实现请求/应答模型与Servlet交互。
Servlet是Web Server或Application Server的一部分,其提供基于请求/响应模型的网络服务,解码基于MIME的请求,并且格式化基于MIME的响应。Servlet容器也包含了管理Servlet生命周期的能力,Servlet是运行在Servlet容器上的。Servlet也可以嵌入宿主的Web Server中,或者通过Web Server的本地扩展API单独作为附加组件安装。Servlet容器也可能内嵌或安装到包含Web功能的Application Server中。

6.2 Servlet 3.0提供的异步处理能力

Wen应用程序中提供异步处理最基本的动机是处理需要很长时间才能完成的请求。 这些耗时较长的请求可能会快速耗尽Servlet容器线程池中的线程并影响应用的可伸缩性。
在Serlet3.0规范前,Servlet容器对Servlet都是以每个请求对应一个线程这种1:1的模式进行处理。如图(本节的Servlet容器固定使用Tomcat来进行讲解)

image.png 由图6-1可知,每当用户发起一个请求时,Tomcat容器就会分配一个线程来运行具体的Servlet。在这种模式下,当在Servlet内执行比较耗时的操作时,当前分配给Servlet的线程就会一直被该Servlet占有,不能被其他请求使用。 而Tomcat内的容器线程池内线程是有限的,当线程池内的线程耗尽后,就不能再处理新的请求,所以这大大限制了服务器能提供的并发请求数量。
为了解决上述问题,Servlet3.0规范引入了异步处理请求的能力,处理线程可以及时返回容器并执行其他任务。一个典型的异步处理的事件流程如下:

  1. 请求被Servlet容器接收,然后从servlet容器(例如Tomcat)中获取一个线程来执行,请求被流转到Filter链来进行处理,然后查找具体的Servlet进行处理。
  2. Servlet具体处理请求参数或者请求内容来决定请求的性质。
  3. Servlet内使用“req.startAsync()"开启异步处理,返回异步处理上下文AsyncContext对象,然后开启异步线程(可以是Tomcat容器中的其他线程,也可以是业务自己创建的线程)对请求进行具体处理(这可能会发起一个远程rpc调用或者一个数据库请求);开启异步线程后,当前Servlet就返回了(分配给其执行的容器线程执行也就释放了),并且不对请求返回产生响应结果。
  4. 异步线程对请求处理完毕后,会通过持有的AsyncContext对象把结果写回请求方

如图6-2所示,具体处理请求响应的逻辑已经不再是Servlet调用线程来做了,Servlet内开启异步处理后会立刻释放Servlet容器线程,具体对请求进行处理与响应的是业务线程中的线程。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92d48802131840c99e4e0ea67d3ba185~tplv-k3u1fbpfcp-watermark.image?)

下面看看SpringBoot中新增一个Servlet时,如何设置其为异步处理。首先,看看一个同步处理的代码。
注:要加上@ServletComponentScan注解将自定义的Servlet注册到Spring容器,否则不会以下代码不会生效

@WebServlet(urlPatterns = {"/test"},loadOnStartup=1)
public class MyServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("--begin  servlet---");
        try {
            //2.执行业务逻辑
            Thread.sleep(3000);

            //3.设置响应结果
            resp.setContentType("text/html");
            PrintWriter out = resp.getWriter();
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Hello World</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>welcome this is my servlet!!!</h1>");
            out.println("</body>");
            out.println("</html>");
        } catch (Exception e) {
            System.out.println(e.getLocalizedMessage());
        } finally {
        }
        //4.运行结束,即将释放容器线程
        System.out.println("---end servlet---");
    }
}

如上代码所示是一个典型的Servlet,当我们访问http://127.0.0.1:8080/test 时,Tomcat容器会接收该请求,然后从容器线程池中获取一个线程来激活容器的Filter链,然后把请求路径路由到MyServlet,此时MyServlet的service方法会被调用。MyServlet内都是同步执行的,都是用同一个Tomcat容器中的线程。

开启异步支持

@WebServlet(urlPatterns = {"/test"}, asyncSupported = true)
public class MyServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            //2.开启异步,获取异步上下文
            System.out.println("--begin  servlet---");
            final AsyncContext asyncContext = req.startAsync();

            //3.提交异步任务
            asyncContext.start(new Runnable() {
                @Override
                public void run() {
                    //3.1 支持业务逻辑
                    System.out.println("--async res begin--");
                    try {
                        Thread.sleep(3000);

                        resp.setContentType("text/html");
                        PrintWriter out = resp.getWriter();
                        out.println("<html>");
                        out.println("<head>");
                        out.println("<title>Hello World</title>");
                        out.println("</head>");
                        out.println("<body>");
                        out.println("<h1>welcome this is my servlet!!!</h1>");
                        out.println("</body>");
                        out.println("</html>");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        //3.3 异步完成通知
                        asyncContext.complete();
                    }
                }
            });
        }catch (Exception e){

        }
        //4.运行结束,即将释放容器线程
        System.out.println("---end servlet---");
    }
}
  • @WebServlet标识MyServlet是一个Servlet,asyncSupported为true代表要异步执行,然后框架就会知道该Servlet要启动异步处理功能
  • MyServlet的Service方法中代码2调用HttpServletRequest的startAsync()方法开启异步调用,该方法返回一个AsyncContext,其中保存了与请求响应相关的上下文信息
  • 代码3调用AsyncContext的start方法并传递一个任务,该方法会马上返回,然后代码4打印后,当前Servlet就退出了,其调用线程(容器线程)也被释放。
  • 代码3提交异步任务后,异步任务的执行还是由容器中的其他线程来具体执行的。代码3.2从asyncContext中获取响应对象。并把响应结果写入响应对象。代码3.3则调用asyncContext.complete()标识异步任务执行完毕。

上面的代码的异步执行虽然及时释放了调用Servlet时的容器线程,但是异步处理还是使用了容器中的其他线程,其实我们可以使用自己的线程池。 改成如下:

@WebServlet(urlPatterns = {"/test"}, asyncSupported = true)
public class MyServlet extends HttpServlet {
    private final static int AVAILABLED_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVAILABLED_PROCESSORS, AVAILABLED_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            //2.开启异步,获取异步上下文
            System.out.println("--begin  servlet---");
            final AsyncContext asyncContext = req.startAsync();
            System.out.println("1.thread:" + Thread.currentThread().getName());
            //3.提交异步任务
            POOL_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    //3.1 支持业务逻辑
                    System.out.println("--async res begin--");
                    System.out.printf("2.thread:" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(3000);

                        resp.setContentType("text/html");
                        PrintWriter out = resp.getWriter();
                        out.println("<html>");
                        out.println("<head>");
                        out.println("<title>Hello World</title>");
                        out.println("</head>");
                        out.println("<body>");
                        out.println("<h1>welcome this is my servlet!!!</h1>");
                        out.println("</body>");
                        out.println("</html>");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        //3.3 异步完成通知
                        asyncContext.complete();
                    }
                }
            });
        } catch (Exception e) {

        }
        //4.运行结束,即将释放容器线程
        System.out.println("---end servlet---");
    }
}

6.3 Servlet3.0 提供的非阻塞IO能力

虽然Servlet3.0规范让Servlet的执行变成了异步,但是其IO还是阻塞式的。IO阻塞是说,在Servlet处理请求时,从ServletInputStream中读取请求体时时阻塞的。 而我们想要的是,当数据就绪时通知我们去读取就可以了,因为这可以避免占用Servlet容器线程或者业务线程来进行阻塞读取。
下面我们来看下什么是阻塞IO:

package com.book.demo.javaAsynchronousProgrammingInAction.chapter6;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author RobbenPeng
 * @since 2023/6/4
 */
@WebServlet(urlPatterns = {"/testSyncReadBody"}, asyncSupported = true)
public class MyServletSyncReadBody extends HttpServlet {
    private final static int AVAILABLED_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVAILABLED_PROCESSORS, AVAILABLED_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            //2.开启异步,获取异步上下文
            System.out.println("--begin  servlet---");
            final AsyncContext asyncContext = req.startAsync();
            System.out.println("1.thread:" + Thread.currentThread().getName());
            //3.提交异步任务
            POOL_EXECUTOR.execute(() -> {
                try {
                    System.out.println("--async res begin--");
                    //3.1 读取请求体
                    long start = System.currentTimeMillis();
                    ServletInputStream inputStream = asyncContext.getRequest().getInputStream();
                    try {
                        byte buffer[] = new byte[1 * 1024];
                        int readBytes = 0;
                        int total = 0;
                        while ((readBytes = inputStream.read(buffer)) > 0) {
                            total += readBytes;
                        }
                        long cost = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread()
                                .getName() + " Read: " + total + " bytes.cost:" + cost);
                    } catch (IOException ex) {
                        System.out.println(ex.getLocalizedMessage());
                    }
                    //3.2 执行业务逻辑
                    Thread.sleep(3000);

                    //3.3设置响应结果
                    resp.setContentType("text/html");
                    PrintWriter out = resp.getWriter();
                    out.println("<html>");
                    out.println("<head>");
                    out.println("<title>Hello World</title>");
                    out.println("</head>");
                    out.println("<body>");
                    out.println("<h1>welcome this is my servlet!!!</h1>");
                    out.println("</body>");
                    out.println("</html>");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } finally {
                    //3.3 异步完成通知
                    asyncContext.complete();
                }
            });
        } catch (Exception e) {

        }
        //4.运行结束,即将释放容器线程
        System.out.println("---end servlet---");
    }
}

可以用postman上传一份文件测试下像这样:

image.png

image.png

而ServletInputStream中并非一开始就有数据,所以当我们的业务线程池POOL_EXECUTOR中的线程调用inputStream.read方法时是会被阻塞的,等内核收到请求方发来的数据后,该方法才会返回,而这之前POOL_EXECUTOR中的线程会一直被阻塞,这就是我们所说的阻塞IO

image.png 如图6-3所示,Servlet容器接收请求后会从容器线程池获取一个线程来执行具体Servlet的Service方法,由Service方法调用StartAsync把请求处理切到业务线程池内的线程,如果业务线程内调用了ServletInputStream的read方法读取http的请求体内容,则业务线程会以阻塞方法读取IO数据(因为数据还没就绪)。当数据还没准备就绪就分配了一个业务线程来阻塞等待数据就绪,造成资源浪费。

Servlet3.1 是如何让数据就绪时才分配业务线程来进行数据读取?

在Servlet3.1规范中提供了非阻塞IO处理方法,Servlet3.1允许我们在ServletInputStream上通过函数setReadListner注册一个监听器,该监听器在发现内核有数据时才会进行回调处理函数。

上面代码注册监听器后的形式如下:

package com.book.demo.javaAsynchronousProgrammingInAction.chapter6;

import javax.servlet.AsyncContext;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author RobbenPeng
 * @since 2023/6/4
 */
@WebServlet(urlPatterns = {"/testSyncReadBody"}, asyncSupported = true)
public class MyServletSyncReadBody extends HttpServlet {
    private final static int AVAILABLED_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVAILABLED_PROCESSORS, AVAILABLED_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //2.开启异步,获取异步上下文
        System.out.println("--begin  servlet---");
        final AsyncContext asyncContext = req.startAsync();
        final ServletInputStream inputStream = req.getInputStream();
        inputStream.setReadListener(new ReadListener() {
            @Override
            public void onError(Throwable throwable) {
                System.out.println("onError:" + throwable.getLocalizedMessage());
            }

            /**
             * 当数据准备好时,通过我们来读
             *
             * @throws IOException
             */
            @Override
            public void onDataAvailable() throws IOException {
                try {
                    long start = System.currentTimeMillis();
                    ServletInputStream inputStream = asyncContext.getRequest().getInputStream();
                    try {
                        byte buffer[] = new byte[1 * 1024];
                        int readBytes = 0;
                        while (inputStream.isReady() && !inputStream.isFinished()) {
                            readBytes += inputStream.read(buffer);
                        }
                        System.out.println(Thread.currentThread().getName() + " Read: " + readBytes);
                    } catch (IOException ex) {
                        System.out.println(ex.getLocalizedMessage());
                    }
                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                }
            }

            /**
             * 当请求体的数据全部被读取完毕后,通知我们进行业务处理
             *
             * @throws IOException
             */
            @Override
            public void onAllDataRead() throws IOException {
                //3.2提交异步任务
                POOL_EXECUTOR.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            System.out.println("--async res begin--");
                            //3.2.1 执行业务逻辑
                            Thread.sleep(3000);
                            //3.2.2 设置响应结果
                            resp.setContentType("text/html");
                            PrintWriter out = asyncContext.getResponse().getWriter();
                            out.println("<html>");
                            out.println("<head>");
                            out.println("<title>Hello World</title>");
                            out.println("</head>");
                            out.println("<body>");
                            out.println("<h1>welcome this is my servlet!!!</h1>");
                            out.println("</body>");
                            out.println("</html>");

                            System.out.println("--async res end--");
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        } finally {
                            asyncContext.complete();
                        }
                    }
                });
            }
        });
        //4.运行结束,即将释放容器线程
        System.out.println("---end servlet---");
    }
}

image.png

  • 代码3设置了一个ReadListener到ServletInputStream流,当内核发现有数据已经就绪时,就会回调器onDataAvailable方法,该方法内就可以马上读取数据。这里代码3.1通过inputStream.isReady()发现数据已经准备就绪后,就可以从中读取数据了。需要注意的是,这里的onDataAvailable是容器线程来执行的,只有在数据已经就绪时才调用容器线程来读取数据。
  • 另外,当请求体的数据全部读取完毕后才会调用onAllDataRead()方法,该方法默认也是容器线程来执行的。这里我们使用代码3.2切换到业务线程池来执行。

image.png 如图6-4所示,Servlet容器接收到请求后会从容器线程池获取一个线程来执行具体的Servlet的Service方法,Service方法内调用StartAsync开启异步处理,然后通过setReadListener注册一个ReadListener到SerletInputStream,最后释放容器线程。
当内核发现TCP接收缓存有数据时,会回调ReadListener的onDataAvailable方法,这时使用的是容器的线程,但是我们可以选择在onDataAvailable方法内开启异步线程来对就绪数据进行读取,以便及时释放容器线程
当发旋现http的请求内容被读取完毕后,会调用onAllDataRead方法,在这个方法内我们使用业务线程池对请求进行处理,并把结果写回请求方
结合上文可知,无论是容器线程还是业务线程,都不会出现阻塞IO的情况。因为当线程被分配来进行处理时,当前数据已经是就绪的,可以马上读取,故不会造成线程的阻塞。

Servlet3.1还增加了可以避免阻塞写的WriteListener接口,可通过setWriteListerner方法进行设置。当WriteListener注册到ServletOutputStream后,当可以写数据时onWritePossible()方法将被容器首次调用。

6.4 Spring MVC的异步处理能力

Spring MVC 的中央Servlet DispatcherServlet为请求处理提供共享的路由算法,负责对请求进行 路由分派,实际的请求处理工作由可配置的委托组件执行。

Spring MVC内部通过调用request.startAsync()将ServletRequest置于异步模式。这样做的主要目的是Servlet(以及任何Filter)可以退出(同时容器线程也得到了释放),但响应保持打开状态,以便进行后续处理(异步处理完毕后使用其把结果写回请求方)。

SpringMVC内部对request.startAsync()的调用返回AsyncContext,可以使用它来进一步控制异步处理。例如,它提供了dispatch方法,类似于ServletAPI中的forward,不同的是它允许应用程序在Servlet容器线程上恢复请求处理。

package com.book.demo.javaAsynchronousProgrammingInAction.chapter6;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author RobbenPeng
 * @since 2023/6/4
 */
@RestController
@RequestMapping()
public class Controller {
    private static ThreadPoolExecutor BIZ_POOL = new ThreadPoolExecutor(8, 8, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1), new ThreadPoolExecutor.CallerRunsPolicy());

    @PostMapping("/personDefferedResult")
    DeferredResult<String> listPostDefferResult() {
        DeferredResult<String> deferredResult = new DeferredResult<>();
        BIZ_POOL.execute(() -> {
            try {
                //执行异步处理
                Thread.sleep(3000);
                //设置结果
                deferredResult.setResult("ok");
            } catch (Exception e) {
                e.printStackTrace();
                deferredResult.setErrorResult("error");
            }
        });
        return deferredResult;
    }
}

上述代码整个处理过程如下:

  1. Tomcat容器接收路径为personDeferredResult的请求后,会分配一个容器线程来执行DispatcherServlet进行请求分派,请求被分到含有personDeferredResult路径的controller,然后执行listPostDeferredResult方法,该方法内创建了一个DeferredResult对象,然后把处理任务提交到了线程池进行处理,最后返回DeferredResult对象。
  2. Spring MVC内部在personDeferredResult方法返回后会保存DeferredResult对象到内存队列或者列表,然后会调用request.startAsync()开启异步处理,并且调用DeferredResult对象的setResultHandler方法,设置当异步结果产生后对结果进行重新路有的回调函数(逻辑在WebAsyncManager的startDeferredResultProcessing方法),接着释放分配给当前请求的容器线程,与此同时当前请求的DispatcherServlet和所有filters也执行完毕了,但是response流还是保持打开(因为任务执行结果还没写回)。
  3. 最终在业务线程池中执行的异步任务会产生一个结果,该结果会被设置到Deferred对象,然后设置的回调函数会被调用,接着Spring MVC会被分派请求结果回到Servlet容器继续完成处理,DispatcherServlet被再次调用,使用返回的异步请求结果。

6.4.2 基于Callable实现异步处理

@PostMapping("/personPostCallable")
    public Callable<String> listPostCall() {
        System.out.println("---begin personPostCallable");
        return () -> {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("----end personPostCallable");
            return "test";
        };
    }

上述代码controller内的listPostCall方法返回了一个异步任务后就直接返回了,其中的异步任务会使用Spring框架内部的TaskExecutor线程池来执行。 整个执行流程如下:

  1. Tomcat容器接收路径为personPostCallable的请求后,会分配一个容器线程来执行DispatcherServlet进行请求分派,请求被分到含有personPostCallable路径的controller,然后执行listPostCall方法,返回一个Callable对象
  2. Spring MVC内部在listPostCall方法返回后,调用request.startAsync()开启异步处理,然后提交Callable任务到内部线程池TaskExecutor(非容器线程)中进行异步执行(WebAsyncManager的startCallableProcessing方法内),接着释放分配给当前请求的容器线程,与此同时当前请求的DispatcherServlet和所有filters也执行完毕了,但是response流还是保持打开(因为任务执行结果还没写回)
  3. 最终在线程池TaskExecutor中执行的异步任务会产生一个结果,然后Spring MVC会分派请求结果回到Servlet容器继续完成处理,DispatcherServlet被再次调用,使用返回的异步结果继续进行处理,最终把响应结果写回请求方。

这种方式下默认使用的是SimpleAsyncTaskExecutor,其对每一个请求都会开启一个线程,并没有很好地服用线程。我们可自定义自己的线程池来执行异步处理:

package com.book.demo.javaAsynchronousProgrammingInAction.chapter6;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author RobbenPeng
 * @since 2023/6/5
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(8);
        executor.setCorePoolSize(8);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setQueueCapacity(5);
        executor.afterPropertiesSet();
        configurer.setTaskExecutor(executor);
    }
}

第7章 Spring WebFlux的异步阻塞处理

第8章 高性能异步编程框架和中间件

8.1 异步、基于事件驱动的网络编程框架——Netty

8.2 高性能RPC框架——Apache Dubbo

8.3 高性能线程间消息传递库——Disruptor

8.4 异步、分布式、基于消息驱动的框架——Akka

8.5 高性能分布式消息框架——Apache RocketMQ