JUC 之 FutureTask 源码与工作原理分析
JDK1.5 引入了Future模式,Future代表了一个异步任务的执行结果。Future模式可以理解成:主线程将待执行的任务提交给子线程执行后,可以先获取任务结果的持有者Future。然后主线程可以去执行其他的任务。等待到要关注之前任务的执行结果时,再从Future中获取。下面是用户注册的场景。
同步实现时,注册接口响应时间150ms= 用户信息保存50ms + 发送短信50ms + 发送邮件50ms
异步实现时,注册接口响应时间100ms= 用户信息保存50ms + max(发送短信50ms, 发送邮件50ms)
Future 接口核心方法
后面所有分析全部基于JDK8
在JDK中Future接口的实现类为FutureTask,其UML图如下。
FutureTask同时实现了Runnable与Future的功能。实现Runnable主要是为了让Executor可以执行,实现Future为了能够持有执行结果的引用。
FutureTask的成员变量如下:
先来看一下用于维护当前任务执行状态的state成员变量。
FutureTask内部定义了,任务执行的7种状态。
上图展示了,Future任务状态的扭转图。new状态时通过调用set()方法、setException()方法可以使状态最终分别转变为NORMAL与EXCEPTIONAL; new状态时通过调用cancel(flase)方法、cancel(true)方法可以可以使状态最终分别转变为CANCELLED与INTERRUPTED。
再看一下用于保存由于调用Future.get方法而阻塞的线程的Treiber椎引用成变量waiters。
每当一个线程调用Future.get去获取任务的执行结果时,如果当前任务还没有执行结束、还没有被取消或者执行中未抛出异常。将产生一个新的WaitNode类型的节点,该节点持有调用Future.get方法的线程的引用,放入到Treiber椎中。FutureTask成变量waiters更新为最新的WaitNode节点。如下图。
介绍了,FutureTask中最重要的两个成量变state与waiters。现在开始分析FutureTask的源码与工作原理。先从FutureTask的get方法入手。
FutureTask awaitDone方法:
从上面的分析中可以看出Future中任务没有执行完(包括正常执行、执行中抛出现异常、执行任务被取消)时,调用get方法的线程被LockSupport.park方法挂起,操作系统将不会对这个线程进行调度,当前线程被阻塞。而这个阻塞的结束是依靠调用LockSupport.unpark方法。LockSupport.unpark方法只有在任务正常执行完、执行中抛出现异常、执行任务被取消才会被调用。先不看具体何时在什么地方LockSupport.unpark方法被调用。先看看report方法的源码。
上面已将FutureTask的get方法有关的源码分析了一遍。但任务对应的结果outcome是何时在什么被设置呢?下面分析一下这部分,即任务的执行结果outcome如何被设置的,何时被设置的。FutureTask中有一个set方法源码如下。
finishCompletion源码如下。
跟踪调用链会发现run方法里面调用了set方法,由于FutureTask实现了Runnable接口,实现了run方法。run源码如下。
前面曾经提到FutureTask实现Runnable主要是为了让Executor可以执行,实现Future为了能够持有执行结果的引用。
ExecutorService扩展了Executor提供了三个重载的submit方法,从而让线程池支持Future模式。
如果提交的任务类型是Runnable在AbstractExecutorService类中,Runnable类型的任务将适配为Callable类型的任务。源码如下,不做具体分析。后面分析线程池源码时会分析。
对于任务的调用记住以下两点便可:
如果submit给线程池的任务类型为Callable,则线程池内部执行该任务的线程的run方法,会调用FutureTask的run方法,而FutureTask的run方法又会调用提交任务的Callable的call方法。
如果submit给线程池的任务类型为Runnable,则线程池内部执行该任务的线程的run方法,会调用FutureTask的run方法,而FutureTask的run方法又会调用提交任务适配后的Callable的call方法,而Callable的call方法内部调用Runnable的run方法。
最后看一下FutureTask的cancel方法
上面对Future做了这整体分析。回顾几个重要的知识点:
当提交任务没有执行完时,Future的内部通过Treiber椎与LockSport.park将调用Future.get方法的线程构成一个WaitNode加入到Treiber椎中挂起,当任务执行完成后(包括正常执行、执行中抛出现异常、执行任务被取消),将遍历Treiber椎中的结点调用LockSport.unpark唤醒所有挂起的线程让其返回任务的执行结果。
FutureTask类提供了done方法做为扩展点,提交任务成功执行后(包括正常执行、执行中抛出现异常、执行任务被取消)done方法将被调用。可以利用这个扩展点实现回调,不用调用isDone与get组合去轮询任务的执行结果,而在任务执行回后会主动调用之前的回调方法。(Google的Guava库利用这个扩展点实现了ListenableFuture,同时Spring-core中也借鉴了Guava的ListenableFuture实现了ListenableFuture。)
提交的任务,正常执行完、执行中抛出现异常、执行任务被取消,都视为任务执行完成。
调用线程池的submit方法的任务最终将封装成FutureTask,提交给线程池去执行。对于提交类型为Runnable的任务其会被先适配为Callable类型的任务。
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 java烂猪皮 』,不定期分享原创知识。
同时可以期待后续文章ing🚀
作者:叶易
评论