java future 繁琐_Java并发 — Future模式

Java多线程编程我们肯定都不陌生,通常我们有两种方式来让一个新的线程帮助我们完成某个任务:

1、继承Thread类,重写其run方法

2、实现Runnable接口,作为Thread类的构造参数创建Thread实例

这两种方式本质上是一样的。如果被多继承限制则考虑使用实现Runnable接口,如果需要在多个Thread实例中共享数据考虑使用继承Thread类,具体如何选择视具体情况来定。

不过无论是哪种方式,我们都无法让任务返回一个结果,run方法执行完之后线程会被JVM自动回收。如果我们要想收集所有线程的运行结果,则只能使用共享变量的方式,例如让所有的线程将处理结果都存储到同一个队列中:

如果仅仅这样就能解决我们的问题的话,倒也可以接受。然而在实际应用中,除了要获得线程的处理结果以外,我们还要能够随时感知任务当前的状态(不是线程当前的状态),即我们的任务是在处理中,还是已经处理完成,其中处理完成又可以分为异常退出和正常退出,我们如何区分呢?

综上所述,我们可以得出一个结论,即传统的并发模型只能用于简单的编程模式,在复杂的并发编程模型下,我们没有办法简单的拿来即用。为了解决上述问题,JDK1.5推出了Future多线程模式,让我们既能随时随地获得任务的执行状态,又能够方便的获得任务的返回结果。

一、Future和Callable

Future接口定义了对于一个任务的基本操作:泛型定义了任务的结果类型、cancel()、isCancelled()、isDone()等方法定义了对任务状态的基本操作、get()和get(time)定义了对于任务结果的获取方式

Callable接口则定义了任务的本身:

Runnable同样定义了任务的本身,不过其任务的入口为void run(),callable定义了一个有返回结果的任务,其任务的入口为 V call()

二、FutureRunnable

从设计模式的角度上来说,接口赋予实现类某种概念,实现类为这些概念提供具体的实现。Callable接口定义了任务本身,Future接口定义了对于任务的基本操作,但仅仅有这些还是不够的。Future模式从本质上来说依然还是Java多线程编程,所以其具体的任务依然是要交给Thread来执行,而Thread可以执行的任务必须继承Runnable接口,所以一个新的接口就这么被组合了出来:

RunnableFuture接口同时继承了Runnable接口和Future接口,使得该接口的实现类既可以作为Thread的一个任务来执行,同时又满足Future的设计。

三、FutureTask

RunnableFuture接口的实现类虽然既可以当做任务来被Thread执行,又满足Future的基本设计,但是他的任务入口依然是void run()方法,前面我们提到过,具有返回值的任务应该实现的是Callable接口,所以RunnableFuture的实现类中组合了Callable接口,其中最典型的一个实现类就是FutureTask:

通过源码可以看到FutureTask中不仅组合了Callable接口,还定义了任务当前的状态。

综上所述,我们可以很清晰的构建出一幅依赖关系图:

从源码中可以看到FutureTask有两种构造函数

以Runnable和Result为构造参数的构造方式,其内部实则创建了一个Callable接口的实现类RunnableAdapter作为callable对象,并且其call方法返回的值固定为我们传入的构造参数Result。之所以提供这个构造函数,是因为在有些情况下,我们其实并不关心每个任务的返回值,而只关心任务是否已经完成等,即关注点在于任务的状态,此时提供一个统一的返回结果未尝不是一个良好的设计。

Callable接口作为构造参数例子:

Runnable接口和泛型结果作为构造参数例子:

无论是哪一种构造函数,使用起来都很简单,他们的不同点就是前面提到的对于任务的返回值的定位不同。下面先来看一下FutureTask作为一个Thread可执行的任务,其任务的入口,即run方法的源码:

在run方法中调用了Callable的call()方法,并且将执行过程中遇到的异常或者是执行成功返回的结果设置到当前FutureTask中保存下来。如果是异常任务则outcome中保存的是执行过程中捕获到的Exception,并且任务状态置为EXCEPTIONAL,如果是正常任务则outcome中保存的是Callable返回的执行结果,并且任务状态置为NORMAL:

通过get()方法我们可以获取到任务执行的结果,如果尚未执行完则阻塞当前线程直到任务执行结束。

任务正常执行时,任务状态为NORMAL,outcome保存的是任务的结果,正常返回。任务异常执行时,outcome保存的是任务的异常信息,抛出该异常。

通过上面的代码片段我们可以看到,FutureTask的原理很简单,就是通过线程执行Runnable接口或Thread类的run方法的特性,来间接执行Callable接口的call方法,并且在此基础上提供了一套完善的任务生命周期管理功能让开发者可以只需要关注任务的具体逻辑。

四、Future模式编程实战

在了解Future模式的基本设计以及Java对于Future模式的实现细节后,我们来使用FutureTask进行一个编程实战增加我们更加熟悉Future模式编程的基本套路以及FutureTask的使用假设我们有一连串的任务需要处理,每个任务耗时大概2秒钟,并且我们要记录所有任务的执行结果,当所有任务都结束时立即打印所有的执行结果,所有任务越快完成越好

通过使用FutureTask,可以很容易的满足上述要求,示例如下

不过这里我只是为了展示Future模式的编程通用套路,在实际编程中我们可不能这么写。因为我们为每一个任务都创建了一个线程,当任务执行耗时较长时,会导致同时存在大量的线程,同传统的多线程编程一样,我们也需要用线程池来管理我们的任务,JDK依然为Future模式下的任务提供了线程池支持:

如上图所示,红色框圈出来的内容就是线程池提供给我们的提交Callable任务的接口。下面我们可以简单的通过线程池来改善一下上面的程序示例

如上图所示,创建了一个固定大小为3个线程的线程池,即同一时间最多存在3个存活的线程,可以防止任务队列过大时将我们的内存搞爆的尴尬情况。通常情况下上图示例中的编程套路已经可以解决我们大部分实际中的工作了,不过线程池的使用远没有这么简单,但不属于本文的范围,就不多说了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值