Java异步编程实战
认识异步编程
异步编程概念与作用
-
同步编程简单,但是容易出现性能瓶颈;并行多个线程可以提高系统性能,但对共享资源进行访问时引入资源竞争和并发问题
- 使用同步阻塞的编程方式会浪费资源
- 通过增加单机系统的线程个数的并行编程方式并不是最好解决方式。异步、非阻塞的代码可以进一步提高系统性能。
-
异步编程是可以让程序并行玉兴的一种手段,其可以让程序中的一个工作但与与主应用程序线程分开独立余小宁,并且在工作单元运行结束后,会通知主应用程序线程他的运行结果或者失败原因。
- 使用异步编程可以提高应用程序的性能和响应能力等
异步编程场景
-
需要异步处理一些事情,而不需要知道异步任务的结果
- 例如在调用栈里异步打印日志,为了不让日志打印阻塞调用线程,会把日志设置为异步方式(基于一个内存队列实现的多生产单消费模型)
-
使用
Java中可以直接开启一个线程来实现,也可以把异步任务封装为任务对象投递到线程池中来执行
在Spring框架中提供了@Async注解把一个任务异步化来进行处理
Future可以获取异步执行的结果,但是会阻塞调用线程,并没有实现完全异步化
JDK8中提供了CompletableFuture弥补Future的缺点。允许以非阻塞方式和基于通知的方式处理结果,其通过设置回调函数方式,让主线程彻底解放出来,实现了实现了意义上的异步处理。
-
JDK8 引入Stream,有效的处理数据流,其使用声明方式编程让我们可以写出可读性、可维护性很强的代码,并且结合CompletableFuture完美的实现异步编程
- 缺点是它产生的流只能使用一次,并且缺少与时间相关的操作(例如RxJava中基于时间窗口的缓存元素),虽然可以执行计算,但无法指定要使用的线程池。同时它也没有设计用于处理延迟的操作(例如RxJava中的defer操作)
- Reactor、RxJava等Reactive API就是为了解决这些问题而生的
Reactor、RxJava等反应式API也提供了Java 8 Stream的运算符,但他们更适用于流序列,并允许定义一个转换操作的管道,该管道应用于通过它的数据。Reactive旨在处理同步或异步操作,并允许你对元素进行缓冲(buffer)、合并(merge)、连接(join)等各种转换。
-
上面都是单JVM内的异步编程,跨网络的交互式
- 例如线程A顺序的对多个服务请求进行调用,简单但是浪费资源。好的做法应该是在发起请求的调用线程发起请求后,注册一个回调函数,然后马上返回去执行其他操作,当远端把结果返回后再使用IO线程或框架线程池中的线程执行回调函数。
-
Java中NIO可以实现网络中的异步调用,高性能异步、基于事件驱动的网络编程框架Netty的出现从繁杂的Java NIO程序中解放出来
- Netty框架将网络编程逻辑与业务逻辑处理分离开来,在内部帮我们自动处理好网络与异步处理逻辑,让我们转移写自己的业务处理罗,而Netty得异步非阻塞能力与CompletableFuture结合则可以轻松的实现网络请求的异步调用。
- 异步调用两个服务,然后借助CompletabelFuture的能力等两次RPC调用都异步返回结果后再执行其他操作。
有了CompetableFuture实现异步编程,可以很自然的使用适配器来实现Reactive风格的编程。当我们使用RxJava API时,只需要使用Flowable的一些函数转换为CompletableFuture为Flowable对象即可。
Servlet 3.0规范让Servlet的执行变为了异步,但是其IO还是阻塞式的
Servlet 3.1规范提供了非阻塞IO来解决这个问题
-
Servlet实现了异步处理与非阻塞IO,但其异步是步彻底的
- 规范是同步的(Filter,Servlet)或阻塞的(getParameter,getPart)
-
WebFlux是与Servlet技术栈并存的一种新技术
- 基于JDK8函数时编程,与Netty实现天然的异步、非阻塞处理
- 使用少量线程和较少的硬件资源来处理并发的非阻塞Web技术栈
-
降低异步编程成本的框架
-
disruptor
- 高性能线程间消息传递库Disruptor,其通过事件预先分配内存、无锁CAS算法、缓冲行填充、两阶段协议提交来实现多线程并发处理不同的元素,从而实现高性能的异步处理
-
Akka
- 基于Actor模式实现了天然支持分布式的使用消息进行异步处理的服务
-
Apache RocketMetaQ
- 实现了应用间的异步解耦、流量削峰
-
-
Go语言
- 语言层面内置的goroutine与channel可以轻松实现复杂的异步处理能力
总结
显示使用线程何线程池实现异步编程
显示使用线程实现异步编程
同步编程模型例子
-
使用Thread创建一个线程来异步执行任务
-
开启线程的方法
- 实现Iainjava.lang.Runnable接口的run方法,然后传递Runnable接口的实现类作为创建Thread时的参数,启动线程
- 实现Thread类,并重写run方法
-
存在的问题
-
生成环境不建议直接创建Thread,因为线程创建与销毁是有开销的,并且没有限制下称的个数
- 线程池中的线程可以被复用,可以大大减少线程创建与销毁开销,可以有效限制创建的县城个数
-
Thread执行的任务没有返回值
- Future解决
-
增加编程负担
- Java提供了很多分装的类库
-
-
实现Thread类,并重写run方法
显示使用线程池实现异步编程
-
如何显示使用线程池实现异步编程
-
把任务放到线程池里异步执行
- CallerRunsPolicy当线程池任务包和,执行拒绝策略时不会丢弃新的任务,而是会使用调用线程来执行
- 使用了命名的线程创建工厂,以便排查问题时可以方便追溯是哪个相关业务
-
-
线程池ThreadPoolExecutro原理剖析
-
概述
-
类图结构
- 子主题 1
-
ThreadPoolExecutor解析
-
成员变量ctl是Integer的原字变量,使用一个变量同时记录线程池状态和线程池中线程个数
- 高3位表示线程池状态,后面29位用来记录线程池线程个数
-
线程池主要状态列举
-
RUNNING
- 接收新任务并且处理阻塞队列里的任务
-
SHUTDOWN
- 拒绝新任务但是处理阻塞队列里的任务
-
STOP
- 拒绝新任务并且抛弃阻塞队列里的任务,同时中断正在处理的任务
-
TIDYING
- 所有任务都执行完(包含阻塞队列里面的任务),当前线程池活动线程位0, 将要调用terminated方法
-
TERMINATED
- 终止状态。terminated方法调用完成以后的状态
-
-
线程池状态之间转换路径
-
RUNNING->SHUTDOWN
- 当显示调用shutdown()方法时,或者因式调用了finalize(),它里面调用了shutdown方法时
-
RUNNING或者SHUTDOWN->STOP
- 当显示调用shutdownNow()方法时
-
SHUTDOWN->TIDYING
- 当线程池和任务队列都为空时
-
STOP->TERMINATED
- 当terminated()hook方法执行完成时
-
-
获取线程池的运行状态和线程池中线程个数
- runStateOf(int c)
- workerCountOf(int c)
- ctlOf(int rs, int wc)
-
线程池的配置参数
-
corePoolSize
- 线程池核心线程个数
-
workQueue
-
用于保存等待执行的任务的阻塞队列
- ArrayBlockQueue
- LinkedBlockingQueue
- SynchronousQueue
- PriorityBlockingQueue
- DelayQueue
- LinkedBlockingDeque
- LinkedTransferQueue
-
-
maximunPoolSize
- 线程池最大线程数量
-
threadFactory
- 创建线程的工厂类
-
defaultHandler
-
饱和策略(拒绝策略),当队列满了并且线程个数达到最大maximunPoolSize后采取的策略
- AbortPolicy(抛出异常)
- CallerRunsPolicy(使用调用者所在线程来运行任务)
- DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)
- DiscardPolicy(默默丢弃,不抛出异常)
-
-
keepAliveTime
- 存货事件。如果当前线程池中的线程比核心线程数量要多,并且是闲置状态的化,这些限制的线程能存活的最大事件
-
-
mainLock
- ReentrantLock,用来控制新增Worker线程时的原子性,termination是锁对应的条件队列,在线程调用awaitTermination时用来存放阻塞的线程
-
-
Worker解析
-
Worker继承AQS和Runnable接口,时具体承载任务的对象。
- Worker继承了AQS,实现了简单不可重入独占锁,其中state=0标示锁未被获取的状态,state=1标示锁已经被获取的状态,state=-1时创建Worker时默认的状态。创建时状态设置为-1是为了避免该线程在运行runWorker()方法前被中断
firstTask记录该工作线程执行的第一个任务
Thread是具体执行任务的线程
-
-
DefaultThreadFactory
- 线程哦工厂,newThread方法是对线程的一个修饰
- poolNumber是一个静态的原字变量,用来统计线程工厂的个数
- threadNumber是用来记录每个线程工厂创建了多少线程
- 这两个值也作为线程池和线程的名称的一部分
-
ThreadPoolExecutro提供了一系列构造函数让我们创建线程池
- 子主题 1
-
-
提交任务到线程池原理解析
-
ThreadPoolExecutor中提交任务到线程池的方法
- void execute(Runnable)
- Future> submit(Runnable)
- Future submit(Runnable Task, T result)
- Future submit(Callable)
-
void execute(Runnable)提交任务到线程池的逻辑
- 代码
- 流程图
-
Future submit(Runnable)提交任务的逻辑
- 代码
- 流程图
-
Future sumit(Runnable Task, T result)逻辑
- 代码
- 流程图
-
-
线程池中任务执行原理解析
当用户线程提交任务到线程池后,在线程池没有执行拒绝策略的情况下,用户线程会马上返回,而提交的任务要么直接切换到线程池中的Worker线程来执行,要么先放入线程池的阻塞队里面
-
Worker线程工厂原理
- 构造函数
-
关闭线程池原理解析
-
线程池中有两种模式的线程池关闭方法
-
shutdown()
- void
-
shutdownnow()
- 返回值为这时队列里面被丢弃的任务列表
-
shutdown()代码逻辑
shutdownNow()代码逻辑
-
-
线程池的拒绝策略解析
当线程池处于包和状态时,再向线程池投递任务,而对于投递的任务如何处理,是由线程池拒绝策略决定的。拒绝策略的执行是execute方法
-
RejectedExecutionHandler接口
- 用来提供对线程池拒绝策略的抽象
-
分类
-
AbortPolicy
- 源码
-
CallerRunsPolicy
- 源码
-
DiscardPolicy
- 源码
-
DiscardOldestPolicy
- 源码
-
-
总结
- 讨论Java中最基础的显示创建线程的方式来实现异步编程,并指出了其存在的三个问题;显示使用线程池来实现异步编程,并且讲解了线程池的实现原理;目前获取值时还是要阻塞调用线程池的,可以用CompletableFuture来解决
基于JDK中的Future实现异步编程
JDK中的Future
-
介绍
- JUC中Future代表着异步计算结果,Future提供了一系列方法用来检查计算结果是否已经完成,提供了同步等待任务执行完成的方法,提供了获取计算结果的方法。
-
类图
- 子主题 1
-
5个接口方法
-
V get()
- 阻塞式等待异步计算任务完成,并返回结果;如果等待过程中有其他线程取消了该任务,则调用线程抛出CancellationException异常;如果再等待结果的过程中有其他线程中断了该线程,则调用线程抛出InterruptedException异常;如果任务计算过程中抛出了异常,则调用线程会抛出ExecutionException异常
-
V get(long timeout, TimeUnit unit)
- 与get()的区别式,调用线程不会一直被阻塞。添加超时能避免调用线程死等地归情况,让调用线程可以及时释放
-
boolean isDone()
- 如果计算任务已经完成则返回true,否则返回false。这里任务完成是指正常完成、由于抛出异常而完成了或者任务被取消了
-
boolean cancel(boolean)
- 尝试取消任务的执行;如果当前任务已经完成或者任务已经被取消了,则尝试取消任务会失败;如果任务还没被执行时调用了取消任务,则任务将永远不会被执行;如果任务已经开始运行了,这是取消任务,则参数将决定是否要将正在执行任务的线程中断,如果为true则要中断,否则标示不中断;当调用取消任务后,再嗲用isDone()方法,后者返回true,随后调用isCancelled()方法也会一直返回true;如果任务不能被取消,比如任务完成后已经被取消了,则该方法返回false
-
boolean isCancelled()
- 如果任务再执行完毕前被取消了,则该方法返回true,否则返回false
-
-
使用建议
- 超时等待配合取消使用
JDK中的FutureTask
-
FutureTask概述
- FutureTask代表了一个可悲取消的异步计算任务,该类实现Future接口
- FutureTask任务的结果只有当任务完成后才能获取,只能通过get系列方法获取,当结果还没出来时,线程调用get系列方法不i被阻塞。一旦任务被执行完成,任务将不能成功其,除非运行时使用了runAndReset方法。FutureTask的任务类型可以时Callable类型或者Runnable类型。FutureTask类型的任务可以被提交到线程池执行
-
FutureTask执行方式
作为Runnable传入Thread中
-
放入线程池
execute()
-
submit()
- 更简单,但是返回的是Future,不是FutureTask
-
FutureTask的类图结构
-
类图
- 子主题 1
-
概要说明
- FutureTask实现Future接口和Runnable接口,所以可以执行任务,可以传到线程池胡总和线程中执行
-
详细说明
-
state变量
是使用volatile关键字修饰的int变量(用来解决多线程下内存不可见问题),用来记录任务状态
-
任务状态枚举
- NEW
- COMPLETING
- NORMAL
- EXCEPTIONAL
- CANCELLED
- INTERRUPTING
- INTERRUPTED
-
状态转换说明
- 一开始任务状态会被初始化为NEW;当通过set、setException、cancel函数设置任务结果时,任务会转换为终止状态;再任务完成过程中,任务状态可能会变为COMPLETING(当结果被使用set方法设置时),也可能会经过INTERRUPTING状态(当使用cancel(true)方法取消任务并中断任务时)。当任务被中断后,任务状态为INTERRUPTED;当任务被取消后,任务状态为CANCELLED;当任务正常终止时,任务状态为NORMAL;当任务执行异常后,任务状态会变为EXCEPTIONAL。
-
在任务运行过程中,任务可能的状态转换路径
-
NEW->COMPETING->NORMAL
- 正常终止流程转换
-
NEW-COMPLETING->EXCEPTIONAL
- 执行过程中发生异常流程转换
-
NEW->CANCELLED
- 任务还没开始就被取消
-
NEW->INTERRUPTING->INTERRUPTED
- 任务被中断
-
-
任务中的终止状态
- NORMAL\EXCEPTIONAL\CANCELLED\INTERRUPTED
callable是由返回值的可执行任务,创建FutureTask对象时,可以通过构造函数传递该任务
-
outcome
- 任务运行的结果,可以通过get系列方法来获取该结果
- outcome没有被volatie修饰,是因为变量state已经被volatie修饰了,这里是借用volatile的内存语义来保证写入outcome时会把值刷新到主内存,读取时会从主内存读取,从而避免多线程下内存不可见问题
-
runner变量
- 记录了运行该任务的线程,这个是FutureTask的run方法内使用CAS函数设置的
-
waiters变量
-
是一个WaitNode节点,是用Treiber stack实现的无锁栈,栈顶元素用waiters代表。栈用来记录所有等待任务结果的线程节点
- 该栈式一个简单的联表,用来记录所有等待结果被阻塞的线程
-
-
构造函数
FutureTask(Callable)
-
FutureTask(Runnable, V)
- 是用适配器模式来做转换
-
额外说明
- FutureTask中是用UNSAFE机制来操作内存变量
-
-
-
FutureTask的run()方法
-
说明
- 该方法时任务的执行体,线程时调用该方法来具体运行任务的,如果任务没有被取消,则该方法会运行任务,并且将结果设置到outcome变量中
- 如果直接运行,则不是在单独线程中主席那个
源码
-
-
FutureTask的get()方法
-
说明
- 等待异步计算任务完成,并返回结果;如果当前任务计算还没完成则会阻塞调用线程直到任务完成;在等待结果的过程中可能出现CancellationException、InterruptedException、ExecutionException异常
源码
awaitDone方法的实现
get(long,TimeUnit)
-
-
FutureTask的cancel(boolean)方法
-
说明
- 尝试取消任务的执行。传true时会中断正在主席那个任务的线程
源码
-
-
总结
- 当我们创建一个FutureTask时,其任务状态初始化为NEW,当我们把任务提交到线程或者线程池后,会有一个线程来执行该FutureTask任务,具体是调用其run方法执行任务。在任务执行过程中,可以在其他线程调用FutureTask的get()方法来等待获取结果,如果当前任务还在执行,则调用get的线程会被阻塞然后放入FutureTask内的阻塞链表队列;毒功而线程可以同时调用get方法,这些可能都会被阻塞并放到阻塞链表队列中。当任务主席那个完毕后会把结果或者异常信息设置到outcome变量,然后会移除和唤醒FutureTask内阻塞链表队列中的线程节点,进而这些由于FutureTask的get方法而被阻塞的线程就会被激活
-
FutureTask的局限性
-
列举
- 并发代码不简洁
- 不能表达多个FutureTask之间的关系
- get方法是阻塞式
-
要求
- 结合多个异步计算,比如独立或者依赖
- 对反应式编程的支持,也就是当任务计算完成后能进行通知,并且可以以计算结果作为一个动作的参数进行下一步计算
- 可以退共国编程的方式手动设置(代码的方式)Future的结果;FutureTask不能实现让用户通过函数来设置其结果,而是在其任务内部来设置
- 可以等多个Future对应的计算结果都出来后做一些事情
-
解决
- JDK8的CompletableFuture
-
JDK中的CompletableFuture
-
概述
-
简介
- CompletableFuture是一个可以通过编程方式设置借宿那结果和状态以便让任务结束的Future,并且其可以作为一个CompletionState,当它的计算完成时可以出发一个函数或者行为;当毒功而线程企图调用同一个CompletableFuture的complete、cancel方式时只有一个线程会成功。
-
额外说明
-
CompletableFuture还实现了CompletionState的一些接口
- 当CompletableFuture任务完成后,同步是用任务执行线程来执行依赖任务结果的函数或者行为
- 所有异步的方法在没有显示指定Esxecutro参数的情况下都是复用ForkJoinPool.commonPool()线程池来执行
- 所有CompletionState方法的实现都是相互独立的,以便一个方法的行为不会因为重载了其他方法而受影响
一个ComletableFuture任务可能有一些依赖其计算结果的行为方法,这些行为方法被手机到一个无所基于CAS操作的链接起来的链表组成的栈中;当CompletableFuture的计算任务完成后,会自动弹出栈中的行为方法并执行
-
-
ForkJoinPool框架
与其他ExecutorService的区别是是用了工作窃取算法来提供性能,其内部每个线程都关联自己的内存队列,正常情况下每个线程从自己队列里面获取并执行,当本身队列没有任务时,当前线程会去其他线程概念来奶的队列里面获取任务来执行。
是用与很多任务会产生子任务或者由很多小任务的情况
commonPool静态线程池
-
commonPool的参数可以通过system properties中的三个参数来控制
-
java.util.concurrent.ForkJoinPool.common.parallelism
- 并行度级别,非负整数
-
java.util.concurrent.ForkJoinPool.common.threadFactory
- ForkJoinWorkerThreadFactory的类名
-
java.util.concurrent.ForkJoinPool.common.exceptionHanderl
- UncaughtExceptionHanderl的类名
-
可以使用构造函数显示设置线程格式,默认时当前机器上可用的CPU个数
提供了任务执行、任务生命周期控制的方法、任务状态检测的方法、toString可以返回当前线程池的状态
当线程池关闭或者内部资源被耗尽,再向线程池提交任务会抛出RejectedExecutionException异常
-
显示设置CompletableFuture结果
-
基于CompletableFuture实现异步计算与结果转换
-
基于runAsync系列方法实现无返回值的异步计算
- 打印日志、异步做消息通知等
- 可以简化编程
- runAsync(Runnable runnable)方法是使用震哥哥JVM内唯一的ForkJoinPool.commonPoool()线程池来执行异步任务的,使用RunAsync(Runnable,Executor)方法允许我们使用自定义的线程池来执行异步任务
-
基于supplyAsync系列方法实现由返回值的异步计算
- 默认线程池同上
-
基于thenRun实现异步任务A,执行完毕后,激活异步任务B执行
- 可以使用thenAcceptAsync来指定设置的回调事件使用自定义线程池来主席那个
thenApply,同上,但是任务B可以拿到任务A的执行结果
-
whenComplete设置回调函数,当异步任务执行完毕后进行回调,不会阻塞调用线程
- 测试时要挂起主线程,等待异步让你无执行完毕
-
总结
- 使用CompletableFuture时,大多数时候不需要显示拆功能键线程池,并传递任务到线程池内,简化编程负担,也可以使用自定义线程池
-
-
多个CompletableFuture进行组合运算
- thenCompose,一个CompletableFuture执行完毕后,执行另外一个CompletableFuture
- thenCombine实现当来给你个并发运行的CompletableFuture任务都完成后,使用两者的结果作为参数再执行一个异步任务
- allOf等待多个并发运行的CompletableFuture任务执行完毕
- anyOf等多个并发运行的CompletableFuture任务中有一个执行完毕
-
异常处理
- CompletableFuture提供了completeExceptionally方法来处理异常情况
CompletableFuture概要原理
JDK8 StreamCompletableFuture
- JDK8 Stream
- 当Stream遇见CompletableFuture
总结
Spring框架中的异步执行
Spring中对TaskExecutor的抽象
如何在Spring中使用异步执行
- 使用TaskExecutor实现异步执行
- 使用注解@Async实现异步执行
@Async注解异步执行原理
总结
基于反应式编程实现异步编程
反应式编程概述
Reactive Stream规范
基于RxJava实现异步编程
基于Reactor实现异步编程
总结
Web Servlet的异步非阻塞处理
Servlet概述
Servlet 3.0提供的异步处理能力
Servlet 3.1提供的非阻塞IO能力
Spring Web MVC的异步处理能力
- 基于DeferredResult的异步处理
- 基于Callable实现异步处理
总结
Spring WebFlux的异步非阻塞处理
Spring WebFlux概述
Reactive编程Reactor库
WebFlux服务器
WebFlux的编程模型
WebFlux的编程模型
- WebFlux注解式编程模型
- WebFlux函数式编程模型
WebFlux原理浅尝
- Reactor Netty概述
- WebFlux服务器启动流程
- WebFlux一次服务调用流程
WebFlux的使用场景
总结
高性能异步编程框架和中间件
异步、基于事件驱动的网络编程框架——Netty
- Netty概述
- Netty的线程模型
- TCP半包与粘包问题
高性能RPC框架——Apache Dubbo
- Apache Dubbo概述
- Dubbo的异步调用
- Dubbo的异步执行
高性能线程间消息传递库——Disruptor
- Disruptor概述
- Disruptor的特性详解
- 基于Disruptor实现异步编程
异步、分布式、基于消息驱动的框架——Akka
- Akka概述
- 传统编程模型存在的问题
- Actor模型解决了传统编程模型的问题
- 基于Akka实现异步编程
高性能分布式消息框架——Apache RocketMQ
- Apache RocketMQ概述
- 基于Apache RocketMQ实现系统间异步解耦
总结
Go语言的异步编程能力
Go语言概述
Go语言的线程模型
- 一对一模型
- 多对一模型
- 多对多模型
- Go语言的线程模型
goroutine与channel
- goroutine
- channel
- 构建管道实现异步编程