Java的异步编程是一项很是经常使用的多线程技术。java
以前经过源码详细分析了ThreadPoolExecutor《你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识》。经过建立一个ThreadPoolExecutor,往里面丢任务就能够实现多线程异步执行了。编程
但以前的任务主要倾向于线程池,并无讲到异步编程方面的内容。本文将经过介绍Executor+Future框架(FutureTask是实现的核心),来深刻了解下Java的异步编程。微信
万事从示例开始,咱们先经过示例Demo有一个直观的印象,再深刻去了解概念与原理。多线程
Demo:
使用上比较简单,并发
运行结果:框架
任务1异步执行:0 任务2异步执行:0 任务2异步执行:1 ... 任务2异步执行:45 同步代码 任务2异步执行:24 ... 任务1异步执行:199 任务1:执行完成 ... 任务2异步执行:199 任务2:执行完成
倘若你屡次执行这个程序,会发现结果大大的不同,由于两个任务和同步代码是异步由多条线程执行的,打印的结果固然是随机的。异步
回顾这个Demo作了什么,ide
其中第二点是异步执行两个任务,这两个任务和主线程分别是用了三个线程并发执行的,第三点是在主线程中同步等待两个任务的结果。异步编程
很容易看出来,异步编程的好处就在于可让不相干的任务异步执行,不阻塞主线程。如果主线程须要异步执行的结果,此时再去等待结果会更加高效,提升程序的执行效率。源码分析
下面来看看整个流程的实现原理。
通常在实际项目中,都会有配置有本身的线程池,建议你们在用异步编程时,配置一个专用的线程池,作好线程隔离,避免异步线程影响到其余模块的工做。Demo中为了方便,直接调用Exectors的方法生成一个临时的线程池,平常不建议使用。
咱们从这个ExecutorService.submit()
方法入手,看看总体实现。
ExecutorService.submit()
定义一个接口。这个接口接收一个Callable参数(执行的任务),返回一个Future(计算结果)。
Callable
,至关于一个须要执行的任务。它不接收任何参数,能够返回结果,能够抛出异常。相相似的还有Runnable
,它也是不接收,不一样点在于它不返回结果,也不抛异常,异常须要在任务内部处理。总结来讲Callable
更像一个方法的调用,Runnable
则是一个不须要理会结果的调用。在JDK 8之后,它们均可以经过Lamda表达式写法去替代内部类的写法(详见Demo)。
Future
,一个异步计算的结果。调用get()
方法能够获得对应的计算结果,若是调用时没有异步计算完,会阻塞等待计算的结果。同时它还提供方法能够尝试取消任务的执行。
看回ExecutorService.submit()
的实现,代码在实现类AbstractExecutorService
中。
除了它接口的实现,还提供了两种变形。原来接口只接收Callable
参数,实现类中还新增了接收Runnable
参数的。
若是看过以前写的《你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识》,应该了解ThreadPoolExecutor
执行任务是能够调用execute()
方法的。而这里面submit()
方法则是为Callable/Runnable
加多一层FutureTask
,从而
使执行结果有一个存放的地方,同时也添加一个能够取消的功能。本来的execute()
只能执行任务,不会返回结果的,具体实现原理能够看看以前的文章分析。
FutureTask
是RunnableFuture
的实现。而RunnableFuture
是继承Future
和Runnable
接口的,定义run()
接口。
由于FutureTask
有run()
接口,因此能够直接用一个Callable/Runnable
建立一个FutureTask
单独执行。但这样并无异步的效果,由于没有启用新的线程去跑,而是在原来的线程阻塞执行的。
到这里咱们清楚知道了,submit()
方法重点是利用Callable/Runnable
建立一个FutureTask
,而后多线程执行run()
方法,达到异步处理而且获得结果的效果。而FutureTask
的重点则是run()
方法如何持有保存计算的结果。
首先判断futureTask
对象的state
状态,若是不是NEW的话,证实已经开始运行过了,则退出执行。同时futureTask
对象经过CAS,把当前线程赋值给变量runner
(是Thread类型,说明对象使用哪一个线程执行的),若是CAS失败则退出。
外层try{}
代码块中,对callable
判空和state
状态必须是NEW。内层try{}
代码真正调用callable
,开始执行任务。若执行成功,则把ran
变量设为true,保存结果在result
变量中,证实已跑成功过了;若抛异常了,则设为false,result
为空,而且调用setException()
保存异常。最后若是ran
为true的话,则调用set()
保存result
结果。
看下setException()
和set()
的实现。
二者的基本流程同样,CAS置换状态,保存结果在outcome
变量道中,但setException()
保存的结果类型固定是Throwable
。另一个不一样在于最终state
状态,一个是EXCEPTION,一个是NORMAL。
这两个方法最后都调用了finishCompletion()
。这个方法主要是配合线程池唤醒下一个任务。
从上面run()
方法得知,最后执行的结果放在了outcome
变量中。那最终怎么从其中取出结果来,咱们来看看get()
方法。
从源码可知,get()
方法分两步。第一步,先判断状态,若是计算为完成,则须要阻塞地等待完成。第二步,若是完成了,则调用report()
方法获取结果并返回。
先看看awaitDone()
阻塞等待完成。该方法能够选用超时功能。
在自旋的for()循环中,
state
值,若是state
大于COMPLETING
,证实计算已是终态了,此时返回终态变量。state
等于COMPLETING
,证实已经开始计算,而且还在计算中。此时为了不过多的CPU时间放在这个for循环的自旋上,程序执行Thread.yield()
,把线程从运行态降为就绪态,让出CPU时间。state
为NEW
,还没开始执行。那么程序在当前循环如今会新增一个WaitNode
,在下一个循环里面调用LockSupport.park()
把当前线程阻塞。当run()
方法结束的时候,会再次唤醒此线程,避免自旋消耗CPU时间。第二步的report()
方法比较简单。
NORMAL
,正常结束的话,则把outcome
变量返回;EXCEPTION
,则把outcome
看成异常抛出(以前setException()
保存的类型就是Throwable
)。从而整个get()
会有一个异常抛出。至此咱们已经比较完整地了解Executor+Future的框架原理了,而FutureTask则是该框架的主要实现。下面总结下要点
Executor.sumbit()
方法异步执行一个任务,而且返回一个Future结果。submit()
的原理是利用Callable
建立一个FutureTask
对象,而后执行对象的run()
方法,把结果保存在outcome
中。get()
获取outcome
时,若是任务未完成,会阻塞线程,等待执行完毕。outcome
中,调用get()
获取结果或抛出异常。更多技术文章、精彩干货,请关注
博客:zackku.com
微信公众号:Zack说码