5.5 Future模式
Future模式是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。当我们需要调用一个函数方法时,如果这个函数执行很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。因此,我们可以让被调者立即返回,让它在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获得需要的数据。
Future模式有点类似在网上买东西。如果我们在网上下单买了一个手机,当我们支付完成后,手机并没有办法立即送到家里,但是在电脑上会立即产生一个订单。这个订单就是将来发货或者领取手机的重要凭证,这个凭证也就是Future模式中会给出的一个契约。在支付活动结束后,大家不会傻傻地等着手机到来,而是可以各忙各的。而这张订单就成为了商家配货、发货的驱动力。当然,这一切你并不用关心。你要做的,只是在快递上门时,开一下门,拿一下货而已。
对于Future模式来说,虽然它无法立即给出你需要的数据。但是,它会返回给你一个契约,将来,你可以凭借着这个契约去重新获取你需要的信息。
如图5.6所示,显示了通过传统的同步方法,调用一段比较耗时的程序。客户端发出call请求,这个请求需要相当长一段时间才能返回。客户端一直等待,直到数据返回,随后,再进行其他任务的处理。
图5-6 传统串行程序调用流程
使用Future模式替换原来的实现方式,可以改进其调用过程,如图5.7所示。
图5-7 Future模式流程图
下面的模型展示了一个广义Future模式的实现,从Data_Future对象可以看到,虽然call本身仍然需要很长一段时间处理程序。但是,服务程序不等数据处理完成便立即返回客户端一个伪造的数据(相当于商品的订单,而不是商品本身),实现了Future模式的客户端在拿到这个返回结果后,并不急于对其进行处理,而去调用了其他业务逻辑,充分利用了等待时间,这就是Future模式的核心所在。在完成了其他业务逻辑的处理后,最后再使用返回比较慢的Future数据。这样,在整个调用过程中,就不存在无谓的等待,充分利用了所有的时间片段,从而提高系统的响应速度。
5.5.1 Future模式的主要角色
为了让大家能够更清晰地认识Future模式的基本结构。在这里,我给出一个非常简单的Future模式的实现,它的主要参与者如表5.2所示。
表5.2 Future模式的主要参与者
它的核心结构如图5.8所示。
图5.8 Future模式结构图
5.5.2 Future模式的简单实现
在这个实现中,有一个核心接口Data,这就是客户端希望获取的数据。在Future模式中,这个Data接口有两个重要的实现,分别是RealData,也就是真实数据,这就是我们最终需要获得的,有价值的信息。另外一个就是FutureData,它就是用来提取RealData的一个“订单”。因此FutureData是可以立即返回得到的。
下面是Data接口:
public interface Data { public String getResult (); }
FutureData实现了一个快速返回的RealData包装。它只是一个包装,或者说是一个RealData的虚拟实现。因此,它可以很快被构造并返回。当使用FutrueData的getResult()方法时,如果实际的数据没有准备好,那么程序就会阻塞,等待RealData准备好并注入到FutureData中,才最终返回数据。
注意:FutureData是Future模式的关键。它实际上是真实数据RealData的代理,封装了获取RealData的等待过程。
public class FutureData implements Data { protected RealData realdata = null; //FutureData是RealData的包装 protected boolean isReady = false; public synchronized void setRealData(RealData realdata) { if (isReady) { return; } this.realdata = realdata; isReady = true; notifyAll(); //RealData已经被注入,通知getResult() } public synchronized String getResult() { //会等待RealData构造完成 while (!isReady) { try { wait(); //一直等待,知道RealData被注入 } catch (InterruptedException e) { } } return realdata.result; //由RealData实现 } }
RealData是最终需要使用的数据模型。它的构造很慢。在这里,使用sleep()函数模拟这个过程,简单地模拟一个字符串的构造。
public class RealData implements Data { protected final String result; public RealData(String para) { //RealData的构造可能很慢,需要用户等待很久,这里使用sleep模拟 StringBuffer sb=new StringBuffer(); for (int i = 0; i < 10; i++) { sb.append(para); try { //这里使用sleep,代替一个很慢的操作过程 Thread.sleep(100); } catch (InterruptedException e) { } } result =sb.toString(); } public String getResult() { return result; } }
接下来就是我们的客户端程序,Client主要实现了获取FutureData,并开启构造RealData的线程。并在接受请求后,很快的返回FutureData。注意,它不会等待数据真的构造完毕再返回,而是立即返回FutureData,即使这个时候FutureData内并没有真实数据。
public class Client { public Data request(final String queryStr) { final FutureData future = new FutureData(); new Thread() { public void run() { // RealData的构建很慢, //所以在单独的线程中进行 RealData realdata = new RealData(queryStr); future.setRealData(realdata); } }.start(); return future; // FutureData会被立即返回 } }
最后,就是我们的主函数Main,它主要负责调用Client发起请求,并消费返回的数据。
public static void main(String[] args) { Client client = new Client(); //这里会立即返回,因为得到的是FutureData而不是RealData Data data = client.request("name"); System.out.println("请求完毕"); try { //这里可以用一个sleep代替了对其他业务逻辑的处理 //在处理这些业务逻辑的过程中,RealData被创建,从而充分利用了等待时间 Thread.sleep(2000); } catch (InterruptedException e) { } //使用真实的数据 System.out.println("数据 = " + data.getResult()); }
5.5.3 JDK中的Future模式
Future模式是如此常用,因此JDK内部已经为我们准备好了一套完整的实现。显然,这个实现要比我们前面提出的方案复杂得多。在这里,我们将简单向大家介绍一下它的使用方式。
首先,让我们看一下Future模式的基本结构,如图5.9所示。其中Future接口就类似于前文描述的订单或者说是契约。通过它,你可以得到真实的数据。RunnableFuture继承了Future和Runnable两个接口,其中run()方法用于构造真实的数据。它有一个具体的实现FutureTask类。FutureTask有一个内部类Sync,一些实质性的工作,会委托Sync类实现。而Sync类最终会调用Callable接口,完成实际数据的组装工作。
图5.9 JDK内置的Future模式
Callable接口只有一个方法call(),它会返回需要构造的实际数据。这个Callable接口也是这个Future框架和应用程序之间的重要接口。如果我们要实现自己的业务系统,通常需要实现自己的Callable对象。此外,FutureTask类也与应用密切相关,通常,我们会使用Callable实例构造一个FutureTask实例,并将它提交给线程池。
下面我们将展示这个内置的Future模式的使用:
01 public class RealData implements Callable<String> { 02 private String para; 03 public RealData(String para){ 04 this.para=para; 05 } 06 @Override 07 public String call() throws Exception { 08 09 StringBuffer sb=new StringBuffer(); 10 for (int i = 0; i < 10; i++) { 11 sb.append(para); 12 try { 13 Thread.sleep(100); 14 } catch (InterruptedException e) { 15 } 16 } 17 return sb.toString(); 18 } 19 }
上述代码实现了Callable接口,它的call()方法会构造我们需要的真实数据并返回。当然这个过程可能是缓慢的,这里使用Thread.sleep()模拟它:
01 public class FutureMain { 02 public static void main(String[] args) throws InterruptedException, ExecutionException { 03 //构造FutureTask 04 FutureTask<String> future = new FutureTask<String>(new RealData("a")); 05 ExecutorService executor = Executors.newFixedThreadPool(1); 06 //执行FutureTask,相当于上例中的 client.request("a") 发送请求 07 //在这里开启线程进行RealData的call()执行 08 executor.submit(future); 09 10 System.out.println("请求完毕"); 11 try { 12 //这里依然可以做额外的数据操作,这里使用sleep代替其他业务逻辑的处理 13 Thread.sleep(2000); 14 } catch (InterruptedException e) { 15 } 16 //相当于5.5.2节中得data.getResult (),取得call()方法的返回值 17 //如果此时call()方法没有执行完成,则依然会等待 18 System.out.println("数据 = " + future.get()); 19 } 20 }
上述代码就是使用Future模式的典型。第4行,构造了FutureTask对象实例,表示这个任务是有返回值的。构造FutureTask时,使用Callable接口,告诉FutureTask我们需要的数据应该如何产生。接着再第8行,将FutureTask提交给线程池。显然,作为一个简单的任务提交,这里必然是立即返回的,因此程序不会阻塞。接下来,我们不用关心数据是如何产生的。可以去做一些额外的事情,然后在需要的时候可以通过Future.get()(第18行)得到实际的数据。
除了基本的功能外,JDK还为Future接口提供了一些简单的控制功能:
boolean cancel(boolean mayInterruptIfRunning); //取消任务 boolean isCancelled(); //是否已经取消 boolean isDone(); //是否已完成 V get() throws InterruptedException, ExecutionException; //取得返回对象 V get(long timeout, TimeUnit unit) //取得返回对象,可以设置超时时间
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论