文章目录
9.1 模式简介
- 假设我们去蛋糕店买蛋糕。下单后,店员一边递给我们提货单,一边说 “请您傍晚再来取蛋糕”。到了傍晚,我们就拿着提货单去取蛋糕。这时,店员会先和我们说 “您的蛋糕已经做好了”,然后将蛋糕递给了我们。
- Future的意思是未来、期货(经济学用语)。假设有一个方法需要花费很长时间才能获取运行结果。那么,与其一直等待结果,不如先拿一张 “提货单”。获取提货单并不耗费时间。这里的 “提货单” 我们就称为 Future 角色。
- 获取 Future 角色的线程会在稍后使用 Future 角色来获取运行结果。这与凭着提货单去取蛋糕非常相似。如果运行结果已经出来了,那么直接领取即可;如果运行结果还没有出来,那么需要等待结果出来。
- Future角色是购买蛋糕时的提货单、预购单、预约券,是 “未来” 可以转化为实物的凭证。
9.3 Future模式中的角色
9.3.1 Client(请求者)
- Client角色向 Host 角色发出请求(request),并会立即接收到请求的处理结果(返回值)——VirtualData角色。
- 不过,这里接收到的 VirtualData 角色实际上是Future角色。也就是说,Future角色戴上了VirtualData角色的面具。Client角色没有必要知道返回值究竟是 RealData 角色还是 Future 角色。稍后,Client角色会通过VirtualData角色来进行操作。
9.3.2 Host
- Host角色会创建新的线程,并开始在新线程中创建RealData角色。同时,它会将Future角色(当作VirtualData角色)返回给Client角色。
- 新线程在创建了RealData角色后,会将其设置到Future角色中。
9.3.3 VirtualData(虚拟数据)
- VirtualData角色是让 Future 角色与 RealData 角色具有一致性的角色。在示例程序中,由 Data 接口扮演此角色。
9.3.4 RealData(真实数据)
- RealData角色是表示真实数据的角色。创建该对象需要花费很多时间。
9.3.5 Future(期货)
- Future角色是RealData角色的 “提货单”,由 Host 角色传递给 Client 角色。从程序行为上看,对Client角色而言,Future角色就是 VirtualData 角色。实际上,当Client角色操作Future角色时,线程会调用 wait 方法等待,直至 RealData 角色创建完成。
- 但是,一旦RealData角色创建完成,线程就不会再继续等待。Future 角色会将Client角色的操作委托给RealData角色。
9.3.6 类图
9.4 拓展思路的要点
9.4.1 吞吐量会提高吗
- 使用Thread-Per-Message模式(第7章)时,我们没有得到处理结果。但是,使用Future模式的话,不仅能保留Thread-Per-Message模式中 “提高程序响应性” 这个优点,还能够获取处理结果。
- 不过,可能大家会有这样的质疑:“我承认Future模式提高了程序响应性,但是它并没有提高吞吐量。如果是从获取到所有结果的最后时间来考虑,无论是单线程运行也好,多线程运行也罢,获取所有结果花费的时间都是相同的。那么,即使将程序修改为多线程运行也无法减少总的处理时间。”
- 这个质疑一半是正确的,一半是错误的。诚然,即使将程序修改为多线程运行,也无法减少总的处理时间。而问题是 “负责长时间处理的线程是哪个线程”。
- 在单CPU服务器上的 Java虚拟机中,如果只是进行多线程计算,吞吐量是无法提高的。这是因为即使让多个线程分担计算任务,运行计算的也只有一个CPU而已。但是,如果结合输入输出(I/O)一起考虑,情况就不同了。例如,当程序在进行磁盘读写时,并不是全部操作都由CPU负责。在程序进行磁盘读写操作时,CPU只是处于等待状态。这时,CPU是有 “空闲时间” 的。如果可以将这些空闲时间分配给其他线程,让它们先进行处理,就可以提高吞吐量。
9.4.2 异步方法调用的 “返回值”
- Java的方法调用全部都是同步的(synchronous)。即一旦调用了某个方法,只有等待该方法执行完毕后才能继续向前执行。
- Thread-Per-Thread模式通过在方法中创建一个新的线程,模拟实现了异步(asynchronous)方法调用。当然,无论怎样使用Thread-Per-Thread模式,Java的方法调用本身仍然是同步的。但是,即便被调用的方法的处理没有全部终止,调用方的处理依然可以继续向前执行,这就是模拟实现异步调用的意义。
- 只使用Thread-Per-Thread模式是无法获取处理结果(即异步方法调用的“返回值”)的。我们可以通过使用Future模式来 “稍后设置处理结果”,从而操作异步方法调用的 “返回值”。
9.4.3 “准备返回值” 和 “使用返回值” 的分离
- 使用 Future 模式可以将 “准备方法的返回值” 与 “使用方法的返回值” 分离开来。接着,我们就可以在其他线程中分别处理 “准备返回值” 和 “使用返回值” 了。
- 将伴随方法调用的一连串处理如同 “慢动作” 一样分解后,把各个处理(启动、执行、准备返回值、使用返回值)分配给各个线程——这就是我们使用多线程这个工具进行的操作。
9.4.4 变种——不让主线程久等的Future角色
- 如果 FutureData 的 getContent 方法被调用时,RealData 的实例还没有创建完成,则使用Guarded Suspension 模式来 “等待创建完成”。正因为有了 “等待创建完成”,主线程从FutureData的 getContent 方法返回时才可以获取到必要的信息。
- 不过,getContent方法的实现也可能是异步的。虽说是异步,但并不像在 getContent 方法中启动新线程那么麻烦。这里可以使用 Balking 模式来实现 “如果还未创建完成就暂时返回”。这样一来,如果RealData的实例没有创建完成,程序的控制权就会暂时返回主线程,然后稍微执行一点其他操作后再去调用getContent
9.4.5 变种——会发生变化的Future角色
- 通常情况下,“返回值” 仅会被设置到 Future 角色中一次。也就是说,Future 角色是 “状态只会改变一次的变量(latch)”。但是,有时也可能会有给Future角色设置 “当前返回值” 的需求。这时,我们可以考虑反复设置 “返回值”。
- 例如,在通过网络获取图像数据时,我们希望在最开始先获取图像的长和宽,接着获取模糊图像数据,最后获取清晰图像数据。此时,Future角色就可能会派上用场。
9.4.6 回调与Future模式
- 如果想要等待处理完成后获取返回值,还可以考虑采用回调处理方式。
- 所谓回调,是指当处理终止后,由 Host 角色启动的线程去调用 Client 角色的方法这一方式。不过,这种情况下,Client角色中就会有与多线程相关的处理。具体而言,必须在Client角色中编写用于安全地传递返回值的代码。
9.6 java.util.concurrent 包与Future模式
- java.util.concurrent.Callable接口将 “返回值的某种处理的调用” 抽象化了。
Callable接口声明了call方法。call 方法与 Runnable 接口的 run 方法相似,不同的是 call 方法有返回值。Callable<string>
这个表达式表示 “Callable接口的 call 方法的返回值的类型是string”。