Java的异步编程是一项非常常用的多线程技术。

之前通过源码详细分析了ThreadPoolExecutor《你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识》。通过创建一个ThreadPoolExecutor,往里面丢任务就可以实现多线程异步执行了。

但之前的任务主要倾向于线程池,并没有讲到异步编程方面的内容。本文将通过介绍Executor+Future框架(FutureTask是实现的核心),来深入了解下Java的异步编程。

万事从示例开始,我们先通过示例Demo有一个直观的印象,再深入去了解概念与原理。

使用示例

Demo:

Java异步编程——深入源码分析FutureTask

使用上比较简单,

运行结果:

任务1异步执行:0

任务2异步执行:0

任务2异步执行:1

...

任务2异步执行:45

同步代码

任务2异步执行:24

...

任务1异步执行:199

任务1:执行完成

...

任务2异步执行:199

任务2:执行完成

假若你多次执行这个程序,会发现结果大大的不一样,因为两个任务和同步代码是异步由多条线程执行的,打印的结果当然是随机的。

回顾这个Demo做了什么,

构建了一个线程池

往线程池里面丢两个需要执行的任务

最后获取这两个任务的结果

其中第二点是异步执行两个任务,这两个任务和主线程分别是用了三个线程并发执行的,第三点是在主线程中同步等待两个任务的结果。

很容易看出来,异步编程的好处就在于可以让不相干的任务异步执行,不阻塞主线程。若是主线程需要异步执行的结果,此时再去等待结果会更加高效,提高程序的执行效率。

下面来看看整个流程的实现原理。

源码分析

一般在实际项目中,都会有配置有自己的线程池,建议大家在用异步编程时,配置一个专用的线程池,做好线程隔离,避免异步线程影响到其他模块的工作。Demo中为了方便,直接调用Exectors的方法生成一个临时的线程池,日常不建议使用。

我们从这个ExecutorService.submit()方法入手,看看整体实现。

Java异步编程——深入源码分析FutureTask

ExecutorService.submit()定义一个接口。这个接口接收一个Callable参数(执行的任务),返回一个Future(计算结果)。

Callable,相当于一个需要执行的任务。它不接收任何参数,可以返回结果,可以抛出异常。相类似的还有Runnable,它也是不接收,不同点在于它不返回结果,也不抛异常,异常需要在任务内部处理。总结来说Callable更像一个方法的调用,Runnable则是一个不需要理会结果的调用。在JDK 8以后,它们都可以通过Lamda表达式写法去替代内部类的写法(详见Demo)。

Future,一个异步计算的结果。调用get()方法可以得到对应的计算结果,如果调用时没有异步计算完,会阻塞等待计算的结果。同时它还提供方法可以尝试取消任务的执行。

看回ExecutorService.submit()的实现,代码在实现类AbstractExecutorService中。

Java异步编程——深入源码分析FutureTask

除了它接口的实现,还提供了两种变形。原来接口只接收Callable参数,实现类中还新增了接收Runnable参数的。

如果看过之前写的《你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识》,应该了解ThreadPoolExecutor执行任务是可以调用execute()方法的。而这里面submit()方法则是为Callable/Runnable加多一层FutureTask,从而

使执行结果有一个存放的地方,同时也添加一个可以取消的功能。原本的execute()只能执行任务,不会返回结果的,具体实现原理可以看看之前的文章分析。

FutureTask是RunnableFuture的实现。而RunnableFuture是继承Future和Runnable接口的,定义run()接口。

Java异步编程——深入源码分析FutureTask

因为FutureTask有run()接口,所以可以直接用一个Callable/Runnable创建一个FutureTask单独执行。但这样并没有异步的效果,因为没有启用新的线程去跑,而是在原来的线程阻塞执行的。

到这里我们清楚知道了,submit()方法重点是利用Callable/Runnable创建一个FutureTask,然后多线程执行run()方法,达到异步处理并且得到结果的效果。而FutureTask的重点则是run()方法如何持有保存计算的结果。

FutureTask.run()

Java异步编程——深入源码分析FutureTask

首先判断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()的实现。

Java异步编程——深入源码分析FutureTask

两者的基本流程一样,CAS置换状态,保存结果在outcome变量道中,但setException()保存的结果类型固定是Throwable。另外一个不同在于最终state状态,一个是EXCEPTION,一个是NORMAL。

这两个方法最后都调用了finishCompletion()。这个方法主要是配合线程池唤醒下一个任务。

FutureTask.get()

从上面run()方法得知,最后执行的结果放在了outcome变量中。那最终怎么从其中取出结果来,我们来看看get()方法。

Java异步编程——深入源码分析FutureTask

从源码可知,get()方法分两步。第一步,先判断状态,如果计算为完成,则需要阻塞地等待完成。第二步,如果完成了,则调用report()方法获取结果并返回。

先看看awaitDone()阻塞等待完成。该方法可以选用超时功能。

Java异步编程——深入源码分析FutureTask

在自旋的for()循环中,

先判断是否线程被中断,中断的话抛异常退出。

然后开始判断运行的state值,如果state大于COMPLETING,证明计算已经是终态了,此时返回终态变量。

若state等于COMPLETING,证明已经开始计算,并且还在计算中。此时为了避免过多的CPU时间放在这个for循环的自旋上,程序执行Thread.yield(),把线程从运行态降为就绪态,让出CPU时间。

若以上状态都不是,则证明state为NEW,还没开始执行。那么程序在当前循环现在会新增一个WaitNode,在下一个循环里面调用LockSupport.park()把当前线程阻塞。当run()方法结束的时候,会再次唤醒此线程,避免自旋消耗CPU时间。

如果选用了超时功能,在阻塞和自旋过程中超时了,则会返回当前超时的状态。

第二步的report()方法比较简单。

Java异步编程——深入源码分析FutureTask

如果状态是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说码

Java异步编程——深入源码分析FutureTask

©著作权归作者所有:来自51CTO博客作者Zack说码的原创作品,如需转载,请注明出处,否则将追究法律责任

java futuretask 源码解析_Java异步编程——深入源码分析FutureTask相关推荐

  1. BAT高级架构师合力熬夜15天,肝出了这份PDF版《Android百大框架源码解析》,还不快快码住。。。

    前言 为什么要阅读源码? 现在中高级Android岗位面试中,对于各种框架的源码都会刨根问底,从而来判断应试者的业务能力边际所在.但是很多开发者习惯直接搬运,对各种框架的源码都没有过深入研究,在面试时 ...

  2. java.lang 源码剖析_java.lang.Void类源码解析

    在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerException if the parent argument is {@code ...

  3. Framework 源码解析知识梳理(5) startService 源码分析

    一.前言 最近在看关于插件化的知识,遇到了如何实现Service插件化的问题,因此,先学习一下Service内部的实现原理,这里面会涉及到应用进程和ActivityManagerService的通信, ...

  4. java socket 重连复用_Java Socket编程基础及深入讲解(示例代码)

    Socket是Java网络编程的基础,了解还是有好处的, 这篇文章主要讲解Socket的基础编程.Socket用在哪呢,主要用在进程间,网络间通信.本篇比较长,特别做了个目录: 一.Socket通信基 ...

  5. 5. Jetpack源码解析---ViewModel基本使用及源码解析

    截止到目前为止,JetpackNote源码分析的文章已经有四篇文章了,这一系列的文章我的初衷是想仔细研究一下Jetpack,最终使用Jetpack组件写一个Demo,上一篇已经分析了LiveData, ...

  6. Java 8中的并行和异步编程

    并行代码是在多个线程上运行的代码,曾经是许多经验丰富的开发人员的噩梦,但是Java 8带来了许多更改,这些更改应该使这种提高性能的技巧更加易于管理. 并行流 在Java 8之前,并行(或并发)代码与顺 ...

  7. Java中如何使用非阻塞异步编程——CompletableFuture

    分享一波:程序员赚外快-必看的巅峰干货 对于Node开发者来说,非阻塞异步编程是他们引以为傲的地方.而在JDK8中,也引入了非阻塞异步编程的概念.所谓非阻塞异步编程,就是一种不需要等待返回结果的多线程 ...

  8. 源码 解析_最详细集合源码解析之ArrayList集合源码解析

    从今天开始我会将集合源码分析陆陆续续整理,写成文章形成集合源码系列文章,方便大家学习 ArrayList集合源码其实相对比较简单,整个源码结构相对于HashMap等源码要好理解的多:先来看下Array ...

  9. 源码解析2-GUI-绘制引擎(QPainter源码分析 )

    Qt源码解析 索引 Qt图形用户界面 应用程序窗口 Qt GUI 模块中最重要的类是QGuiApplication和QWindow.想要在屏幕上显示内容的 Qt 应用程序需要使用这些.QGuiAppl ...

最新文章

  1. Mysql 分页语句Limit用法
  2. STM32中GPIO的8种模式
  3. 计算机科学班(原acm班),计算机科学创新实验班(以下简称ACM班)培养计划.doc
  4. 网络安装Centos x64 6.10
  5. 谈我的“先做人,再做技术人员,最后做程序员”
  6. 【整理】史上最强的娱乐大餐———九奔、汉澳、器普。。。。。。
  7. 想和人脑一样智能? IBM 的芯片级模仿才是关键
  8. c语言最长公共子序列_序列比对(二十四)——最长公共子序列
  9. 实验:添加AXI IP到设计
  10. 软件测试的测试数据分析,软件测试结果归纳与分析
  11. 资本纷纷入局,咖啡赛道还能香多久?
  12. 关键词搜索查找工具-批量关键词查找搜索工具-根据关键词自动采集素材软件
  13. char * 与char []区别总结
  14. Android:自定义View实现绚丽的圆形进度条
  15. simpleCaptcha语音验证码实现及其扩展
  16. 企业微信可以取消实名认证吗?如何操作
  17. SQL SELECT(复杂查询) 之 等值查询 内外连接
  18. 机器学习:决策树-基础算法,剪枝,连续值缺失值处理,多变量决策树(附代码实现)
  19. smartlook.tasktray.exe无法找到入口
  20. 无线充电 Qi认证流程详解

热门文章

  1. 破解Navicat for MySql
  2. Webpack 实现 Tree shaking 的前世今生
  3. 假如苹果构建了一个搜索引擎
  4. 三机齐发!五大全球首发的“安卓机皇”4999元起,“安卓之光” 5999元起
  5. 编程网站 Perl.com 被劫,售价 19 万美元
  6. 九问国产操作系统,九大掌门人万字回应!
  7. 我发现了个 Python 黑魔法,执行任意代码都会自动念上一段「平安经」
  8. 从鲁班造木鸢到智能控制,图解世界无人机发展简史
  9. NASA 遭攻击,安全 Bug 仍未解决!
  10. 除了敲代码,程序员还需要哪些必备技能?