一个多月没有写东西了,今天想写的也是想记录下来的一些学习及思考结果,记忆能力有限,避免时间长久就忘记了,今天想写的也还是一些基础的东西,为什么我总是关注这些平时码业务代码很少能用到的又比较基础东西呢,主要是因为我觉得可能光写简单的业务代码可能很少有机会会遇到难题,但是也有万一,万一遇到了怎么解决,万变不离其宗,基础知识的深度决定上层建筑的高度,因此,扯正题吧,今天写的是关于JDK里面的Future。
Future模式
Future模式的核心思想是能够让主线程将原来需要同步等待的这段时间用来做其他的事情,这个时候可以更好的利用CPU分片,这点可以这么理解,如果我们申请了线程,但是又没让CPU调度,这岂不是很浪费,我们可以有更好的方法来提高CPU的利用率,也就是让线程本来可以等待(歇息)的时间让线程去做其他的事情,榨干它的剩余价值,当然是以CPU没有打满(100%利用率)为前提,JAVA里面对Future模式的具体实现是JDK1.5开始的JCU包中的Future接口及其实现的定义。
不同的工作方式
上图简单描述了不使用Future和使用Future的区别,不使用Future模式,主线程在invoke完一些耗时逻辑之后需要等待,这个耗时逻辑在实际应用中可能是一次RPC调用,可能是一个本地IO操作等。B图表达的是使用Future模式之后,我们主线程在invoke之后可以立即返回,去做其他的事情,回头再来看看刚才提交的invoke有没有结果。
JAVA中Future模式定义的行为
我们来一起看看JAVA中Future接口的定义:
Future接口定义
接口定义行为,我们通过上图可以看到实现Future接口的子类会具有哪些行为,假设我们已经委托了系统一些执行逻辑,那么对于这个执行逻辑,我们有:
- 我们可以取消这个执行逻辑,如果这个逻辑已经正在执行,提供可选的参数来控制是否取消已经正在执行的逻辑。
- 我们可以判断执行逻辑是否已经被取消。
- 我们可以判断执行逻辑是否已经执行完成。
- 我们可以获取执行逻辑的执行结果。
- 我们可以允许在一定时间内去等待获取执行结果,如果超过这个时间,抛TimeoutException。
线程池中的FutureTask
在JCU中,FutureTask是Future的具体实现,额外实现了Runnable接口,既然实现Runnable接口,那么就满足了Task的行为,于是我们得到了一个可以被用来执行的Future。值得一提的是FutureTask的身份,她是JCU提供的线程池实现用到的任务基本单元,常用线程池的同学都知道,线程池接收两种对象,一个是Runnable任务,一种是Callable任务,两者区别在于前者返回执行结果给外部,后者需要。按照默认线程池是实现ExecutorService接口的,按照ExecutorService接口定义的行为,我们可以将Runnable或Callable任务提交到线程池让其去被执行,而被提交的Runnable或Callable任务都会被包装成FutureTask,丢到任务队列,由线程池的工作线程去执行。
FutureTask任务状态流转
当我们把一个FutureTask丢到线程池任务队列之后,任务后续的生命周期是怎么样的呢?在FutureTask中定义了七种任务状态,我们可以一起看一下:
- NEW:当FutureTask被初始创建的时候的状态。
- COMPLETING:当任务被执行完毕,FutureTask会将执行结果设置给FutureTask的outcome属性,在设置之前会将FutureTask的状态修改为COMPLETING。
- NORMAL:当任务被执行完毕,FutureTask会将执行结果设置给FutureTask的outcome属性,在设置之后会将FutureTask的状态修改为NORMAL。
- EXCEPTIONAL:当任务在被执行的过程中抛了异常,FutureTask会将异常信息设置给FutureTask的outcome属性,在设置之前会将FutureTask的状态修改为COMPLETING,在设置之后会将FutureTask的状态修改为EXCEPTIONAL。
- CANCELLED:当外部想要取消任务,而又不允许当任务正在执行的时候被取消的时候会将FutureTask的状态修改为CANCELLED。
- INTERRUPTING:当外部想要取消任务,同时允许当任务正在执行的时候被取消的时候,会先将FutureTask的状态设置为INTERRUPTING,然后设置执行任务的线程的中断标记位。
- INTERRUPTED:当外部想要取消任务,同时允许当任务正在执行的时候被取消的时候,会先将FutureTask的状态设置为INTERRUPTING,然后设置执行任务的线程的中断标记位,最后将Future的状态设置为INTERRUPTED。
综上,我们也可以总结下FutureTask的状态流转可能流程:
- NEW—>COMPLETING—>NORMAL(任务执行正常)
- NEW—>COMPLETING—>EXCEPTIONAL(任务执行异常)
- NEW—>CANCELLED(不允许执行中的取消)
- NEW—>INTERRUPTING—>INTERRUPTED(允许执行中的取消)
总结
JCU包提供了很好的工具让我们能够快速的开发基于线程池的多线程应用,那么当我们把线程提交到线程池之后,站在单独任务的角度,我们关心的核心问题通常是下面几点:
- 任务超时时间,一方面我们不能够无限期的占用线程资源,另一方面我们不能够让外部无限期的等待,因此timeout变得尤为重要。
- 主动取消任务,假如我们觉得timeout不够灵活,通常场景是当我们在timeout之前已经知道FutureTask不需要再继续为我们工作的时候,我们可以先判断任务是否已经done(isDone),如果没有done,我们可以主动的将任务取消掉,这个时候Future定义的cancel可以派上用场。
- 任务异常信息,还记得我们最初提交的Runnable和Callable么,当任务抛出了异常我们如何get到异常信息呢,FutureTask其实是代理了Runnable和Callable的执行,捕获异常并将异常信息交给outcome,因此通过FutureTask,我们同样可以获得任务内部抛出的异常信息。