当我们要获取扔进线程池的Callable的执行结果时,会调用FutureTask的get方法来获取结果。
概述地讲,get方法里会先通过state变量判断任务是否已跑完,跑完则直接将结果返回。否则就构造等待节点扔进等待队列自旋,阻塞住线程。另一边的线程计算出结果后就会将等待队列里的所有节点依次出队并唤醒线程。
往细一点说,FutureTask有一个volatile的state变量,最初始的状态是new 新建状态,其他状态有completing完成中,normal正常执行完,还有其他取消、中断、异常之类的状态。
FutrueTask的get会先判断state的值是否大于comleting,也就是执行完,是的话就判断state为normal正常的话,就返回outcome结果,为cancel取消的话,就抛取消异常,为异常的话,就抛出outcome记录的异常。
如果state的值表示还没执行完的话,就会进入一个自旋操作,也就是一个死循环。
如果state是completing的话,就通过Thread.yield让出时间片。
否则如果结果已经出来了,那就返回。
否则如果还未构造等待结点,那就构造等待结点,结点里保存了当前线程。
否则如果结点还未入队,那就将结点入队,
否则调用LockSupport的park方法阻塞线程。
这里可以说有一个细节,等待结点入队跟阻塞线程在不同的else分支里,使得结点入队后下一次循环才会阻塞线程。避免判断完state变量后,准备将节点入队,而这时另外的线程执行完任务将所有节点出队了。那么此时将节点入队并阻塞线程的话,就不会再被唤醒了。
执行任务的线程那边在执行完任务后,就把结果或者异常保存到outcome里,修改state的值,并且依次将队列里的结点出队并调用LockSupport的unpark方法唤醒节点里的线程。
按上面的流程来看,get方法大概率会处于以下三种情况
1.get时任务结果已出,那么在最开始判断完state后就可以返回结果或抛出相应异常。
2.get时任务正在执行(state为completing),那么在循环中通过Thread.yield让出时间片,直至结果出为止。个人认为作者是考虑到了任务已经在执行,可能很快就执行完,所以没必要阻塞住线程,以避免线程在等待态和就绪态之间转换的消耗。但我认为大多数任务执行时间并不短,因为很多会去操作数据库。那么,这样子不阻塞住线程,反而增加了一些上下文切换的成本。
3.get时任务还未执行(state为new),构造等待节点,加入队列,调用LockSupport的park方法阻塞线程。(这三个操作依次在不同循环中进行)
下面,贴出对应源码
get方法
run方法会调到finishCompletion方法唤醒等待队列中的线程