Future模式是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。当我们需要调用一个方法时,如果这个方法执行得很慢,那么我们就要进行等待;但是有时候,我们可能并不急着要结果。因此,我们可以让被调用者立即返回,让它在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获得需要的数据。
Future模式有点类似于网上买东西,如果我们网上下单购买了一台手机,当我们支付完成后,手机并没有立即送到家里,但是在电脑上回产生一个订单。这个订单就是将来发货或者领取手机的凭证,这个凭证就是future模式给出的一个契约。在支付结束后,大家不会傻傻的等待手机的到来,而是各忙各的。而这张订单就成为了商家配货、发货的驱动力。当然这一切都不用关心。你要做的,只是快递上门,开门拿货而已。
对于Future模式来说,虽然它无法立即给出你要的数据,但是,它会立即返回给你一个契约,将来,你可以凭着这个契约去重新获取你需要的信息。
通过传统的同步方法调用一段比较耗时的程序,调用过程应该是这样的:客户端发出请求,这个请求要相当一段时间才能返回。客户端一直等待,直到数据返回,随后,再进行其他的任务处理。
使用Future模式替换原来的实现方式,可以改进其调用过程。如下图所示:
上面的模型展示了一个广义的Future对象,从Data_Future对象可以看出,虽然call本身需要很长一段时间处理程序。但是,服务程序不等数据处理完便立即返回客户端一个伪造的数据(相当于商品的订单,而不是商品本身),实现了Future模式的客户端在拿到这个结果后,并不急于对其进行处理,而是调用了其他业务逻辑,充分利用了等待时间,这就是Future模式的核心所在。在完成了其他业务逻辑处理后,最后再使用返回比较慢的Future数据,这样,在整个调用的过程中,就不存在无谓的等待,充分利用了时间,从而提高了系统的响应速度。
Future模式的主要角色
参与者 | 作用 |
Main | 系统启动,调用Client发出请求 |
Client | 返回Data对象,立即返回FutureData,并开启ClientThread线程装配RealData |
Data | 返回数据的接口 |
FutureData | Future数据,构造很快,但是是一个虚拟的数据,需要装配RealData |
RealData | 真实数据,其构造是比较慢的 |
Future模式的简单实现
1 public interface Data { 2 public String getResult(); 3 }
核心接口Data,这就是客户端希望获取到的数据。
1 public class RealData implements Data { 2 3 protected final String result; 4 5 public RealData(String para) throws InterruptedException { 6 //RealData的构造很慢,需要用户等很久,这里使用sleep模拟 7 StringBuffer sb = new StringBuffer(); 8 for (int i = 0;i < 10;i++){ 9 sb.append(para); 10 Thread.sleep(100);//sleep模拟很慢的动作 11 } 12 result = sb.toString(); 13 } 14 @Override 15 public String getResult() { 16 return result; 17 } 18 }
RealData是最终需要使用到的数据模型。它的构造很慢,在这个例子中,我们使用了使线程休眠来模拟了这个过程。
1 public class FutureData implements Data { 2 3 protected RealData realData = null; 4 protected boolean isReady = false; 5 6 public synchronized void setRealData(RealData realData){ 7 if (isReady){ 8 System.out.println("-----------"); 9 return; 10 } 11 System.out.println("........................"); 12 this.realData = realData; 13 System.out.println("realData: " + realData); 14 isReady = true; 15 notifyAll();//realData已经被注入,通知getResult() 16 } 17 18 @Override 19 public synchronized String getResult() { 20 while (!isReady){ 21 try { 22 System.out.println("Waiting"); 23 wait();//等待,直到realData被注入 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 return realData.result; 29 } 30 }
FutureData实现了一个快速返回的RealData的包装。它只是一个包装,或者说是RealData的虚拟实现。因此,它可以很快被构造并返回。当使用FutureData的getResult()方法时,如果实际的数据没有准备好,那么程序就会阻塞,等待RealData准备好并注入FutureData中,才最终返回数据。FutureData是Future模式的关键,它实际上是真实数据的代理,封装了获取RealData的等待过程。
1 public class Client { 2 public Data request(final String queryStr){ 3 final FutureData futureData = new FutureData(); 4 new Thread() { 5 @Override 6 public void run() { 7 try {//realData构造很慢,所以在单独的线程中执行 8 RealData realData = new RealData(queryStr); 9 System.out.println("调用setRealData"); 10 futureData.setRealData(realData); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 } 15 }.start(); 16 System.out.println("futureData : " + futureData); 17 return futureData;//futureData对象会被立即返回 18 } 19 }
Client主要实现了获取FutureData,并开启构造RealData的线程。并在接收请求后,很快的返回FutureData。注意,他不会等待真正的数据构造完毕后再返回,而是立即返回FutureData,即使这个时候FutureData内并没有真实数据。
1 public class Main { 2 //测试 3 public static void main(String[] args){ 4 Client client = new Client(); 5 //这里立即返回的是futureData不是RealData 6 Data data = client.request("name"); 7 System.out.println("请求完毕!"); 8 try { 9 Thread.sleep(1000);//这里使用sleep代替其他任务处理,在处理这些任务的过程中,RealData被创建,充分利用了时间 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 System.out.println("数据 = " + data.getResult()); 14 } 15 }
Main主要负责调用Client发起请求,并消费返回的数据。
输出结果:
futureData : CurrentJava.future.FutureData@4554617c
请求完毕!
Waiting
调用setRealData
........................
realData: CurrentJava.future.RealData@b20568f
数据 = namenamenamenamenamenamenamenamenamename
从输出结果可以看出:客户端发送请求,立即返回了对象FutureData,返回请求完毕,Client的内部线程启动构造真实的数据,由于此时RealData并没有构造完毕,所以构造线程wait处于等待状态,当RealData构造完毕后,唤醒了等待线程,拿到真实数据,并返回。这就是Future模式的调用过程。
JDK中的Future模式
由于Future模式是非常常用的多线程设计模式,因此在JDK中内置了Future模式的实现。这些类在java.util.concurrent包中,其中最重要的是FutureTask类,它实现了Runnable接口,可以作为单独线程运行,在它的run()方法中,通过Sync内部类调用Callable接口,并维护Callable接口的返回对象,使用FutureTask.get()方法时,将返回Callable接口的返回对象。
将上述例子,用JDK自带的Future模式来实现:
首先,Data接口和FutureData就不需要了,JDK已经帮我们实现了;
其次,RealData修改为:
1 public class RealData implements Callable<String> { 2 3 private String para; 4 public RealData(String para){ 5 this.para = para; 6 } 7 @Override 8 public String call() throws Exception { 9 StringBuffer sb = new StringBuffer(); 10 for (int i = 0; i < 10;i++){ 11 sb.append(para); 12 Thread.sleep(100); 13 } 14 return sb.toString(); 15 } 16 }
上述的代码实现了Callable接口,它的call()方法会构造我们需要的真实数据并返回,我们用sleep来模拟其缓慢的过程。
1 public class FutureMain { 2 3 public static void main(String[] args) throws InterruptedException, ExecutionException { 4 //构造FutureTask 5 FutureTask<String> futureTask = new FutureTask<String>(new RealData("a")); 6 ExecutorService es = Executors.newFixedThreadPool(1); 7 es.execute(futureTask);//执行FutureTask,开启线程执行RealData的call() 8 System.out.println(futureTask.isDone()); 9 System.out.println("请求完成!"); 10 Thread.sleep(2000);//sleep代替其他业务逻辑处理 11 System.out.println("数据:" + futureTask.get()); 12 es.shutdown(); 13 } 14 }
上述代码就是Future模式的典型。在第5行,构造了FutureTask对象实例,表示这个任务是有返回值的;同时构造FutureTask时,使用了Callable接口,告诉FutureTask我们需要的真实数据应该如何产生。接着在第7行,将FutureTask提交给线程池,提交完成后,立即回有FutureData返回,然后我们做一些额外的事情(这里使用sleep代替),最后再我们需要的时候通过FutureTask.get()得到实际真实的数据。
除了基本的功能外,JDK还为Future接口提供了一些简单的控制功能:
boolean isCancelled():是否已经取消;
boolean isDone():是否已经完成;
boolean cancel(boolean mayInterruptIfRunning):取消任务;
V get() throws InterruptedException, ExecutionException :取得返回的对象;
V get(long timeout, TimeUnit unit):在规定时间内取得返回的对象。
参考:《Java高并发程序设计》 葛一鸣 郭超 编著: