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

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做了什么,

  1. 构建了一个线程池
  2. 往线程池里面丢两个需要执行的任务
  3. 最后获取这两个任务的结果

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

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

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

源码分析

一般在实际项目中,都会有配置有自己的线程池,建议大家在用异步编程时,配置一个专用的线程池,做好线程隔离,避免异步线程影响到其他模块的工作。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()只能执行任务,不会返回结果的,具体实现原理可以看看之前的文章分析。

FutureTaskRunnableFuture的实现。而RunnableFuture是继承FutureRunnable接口的,定义run()接口。

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

到这里我们清楚知道了,submit()方法重点是利用Callable/Runnable创建一个FutureTask,然后多线程执行run()方法,达到异步处理并且得到结果的效果。而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()。这个方法主要是配合线程池唤醒下一个任务。

FutureTask.get()

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

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

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

在自旋的for()循环中,

  • 先判断是否线程被中断,中断的话抛异常退出。
  • 然后开始判断运行的state值,如果state大于COMPLETING,证明计算已经是终态了,此时返回终态变量。
  • state等于COMPLETING,证明已经开始计算,并且还在计算中。此时为了避免过多的CPU时间放在这个for循环的自旋上,程序执行Thread.yield(),把线程从运行态降为就绪态,让出CPU时间。
  • 若以上状态都不是,则证明stateNEW,还没开始执行。那么程序在当前循环现在会新增一个WaitNode,在下一个循环里面调用LockSupport.park()把当前线程阻塞。当run()方法结束的时候,会再次唤醒此线程,避免自旋消耗CPU时间。
  • 如果选用了超时功能,在阻塞和自旋过程中超时了,则会返回当前超时的状态。

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

  • 如果状态是NORMAL,正常结束的话,则把outcome变量返回;
  • 如果是取消或者中断状态的,则抛出取消异常;
  • 如果是EXCEPTION,则把outcome当作异常抛出(之前setException()保存的类型就是Throwable)。从而整个get()会有一个异常抛出。

总结

至此我们已经比较完整地了解Executor+Future的框架原理了,而FutureTask则是该框架的主要实现。下面总结下要点

  1. Executor.sumbit()方法异步执行一个任务,并且返回一个Future结果。
  2. submit()的原理是利用Callable创建一个FutureTask对象,然后执行对象的run()方法,把结果保存在outcome中。
  3. 调用get()获取outcome时,如果任务未完成,会阻塞线程,等待执行完毕。
  4. 异常和正常结果都放在outcome中,调用get()获取结果或抛出异常。


更多技术文章、精彩干货,请关注

博客:zackku.com

微信公众号:Zack说码

原文地址:https://www.cnblogs.com/zackku/p/10069230.html

时间: 2024-10-08 03:01:48

Java异步编程——深入源码分析FutureTask的相关文章

Java并发编程 ReentrantLock 源码分析

ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(AbstractOwnableSynchronizer)封装的 公平与非公平锁. 所谓公平锁就是指 在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,换句话说也就是先被锁定的线程首先获得锁. 非公平锁正好相反,解锁时没有固定顺序. 让我们边分析源代码边学习如何使用该类 先来看一下构造参数,默认

Java并发编程-AbstractQueuedSynchronizer源码分析

简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abstra

java线程池ThreadPoolExector源码分析

java线程池ThreadPoolExector源码分析 今天研究了下ThreadPoolExector源码,大致上总结了以下几点跟大家分享下: 一.ThreadPoolExector几个主要变量 先了解下ThreadPoolExector中比较重要的几个变量.  corePoolSize:核心线程数量     maximumPoolSize:最大线程数量 allowCoreThreadTimeOut:是否允许线程超时(设置为true时与keepAliveTime,TimeUnit一起起作用)

There is no getter for property named '*' in 'class java.lang.String'之源码分析

There is no getter for property named '*' in 'class java.lang.String',此错误之所以出现,是因为mybatis在对parameterType="String"的sql语句做了限制,假如你使用<when test="username != null">这样的条件判断时,就会出现该错误,不过今天我们来刨根问底一下. 一.错误再现 想要追本溯源,就需要错误再现,那么假设我们有这样一个sql查询

Java NIO——Selector机制源码分析---转

一直不明白pipe是如何唤醒selector的,所以又去看了jdk的源码(openjdk下载),整理了如下: 以Java nio自带demo : OperationServer.java   OperationClient.java(见附件) 其中server端的核心代码: public void initSelector() { try { selector = SelectorProvider.provider().openSelector(); this.serverChannel1 =

死磕 java集合之ConcurrentHashMap源码分析(三)

本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作. public V remove(Object key) { // 调用替换节点方法 return replaceNode(key, null, null); } final V replaceNode(Object key, V

死磕 java集合之LinkedHashSet源码分析

问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashSet支持按元素访问顺序排序吗? 简介 上一节我们说HashSet中的元素是无序的,那么有没有什么办法保证Set中的元素是有序的呢? 答案是当然可以. 我们今天的主角LinkedHashSet就有这个功能,它是怎么实现有序的呢?让我们来一起学习吧. 源码分析 LinkedHashSet继承自HashS

死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)CopyOnWriteArraySet以何种方式保证元素不重复? (5)如何比较两个Set中的元素是否完全一致? 简介 CopyOnWriteArraySet底层是使用CopyOnWriteArrayList存储元素的,所以它并不是使用Map来存储元素的. 但是,我们知道CopyOnWriteArra

死磕 java集合之PriorityQueue源码分析

问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合,集合中的每个元素都有一个权重值,每次出队都弹出优先级最大或最小的元素. 一般来说,优先级队列使用堆来实现. 还记得堆的相关知识吗?链接直达[拜托,面试别再问我堆(排序)了!]. 那么Java里面是如何通过"堆"这个数据结构来实现优先级队列的呢? 让我们一起来学习吧. 源码分析 主要属性