多线程不得不聊的Future类

开发 前端
在高性能编程中,并发编程已经成为了极为重要的一部分。在单核CPU性能已经趋于极限时,我们只能通过多核来进一步提升系统的性能,因此就催生了并发编程。

 

[[392402]]

本文转载自微信公众号「三太子敖丙」,作者三太子敖丙 。转载本文请联系三太子敖丙公众号。

在高性能编程中,并发编程已经成为了极为重要的一部分。在单核CPU性能已经趋于极限时,我们只能通过多核来进一步提升系统的性能,因此就催生了并发编程。

由于并发编程比串行编程更困难,也更容易出错,因此,我们就更需要借鉴一些前人优秀的,成熟的设计模式,使得我们的设计更加健壮,更加完美。

而Future模式,正是其中使用最为广泛,也是极为重要的一种设计模式。今天就跟阿丙了解一手Future模式!

生活中的Future模式

为了更快的了解Future模式,我们先来看一个生活中的例子。

场景1:

午饭时间到了,同学们要去吃饭了,小王下楼,走了20分钟,来到了肯德基,点餐,排队,吃饭一共花了20分钟,又花了20分钟走回公司继续工作,合计1小时。

场景2

午饭时间到了,同学们要去吃饭了,小王点了个肯德基外卖,很快,它就拿到了一个订单(虽然订单不能当饭吃,但是有了订单,还怕吃不上饭嘛)。接着小王可以继续干活,30分钟后,外卖到了,接着小王花了10分钟吃饭,接着又可以继续工作了,成功的卷到了隔壁的小汪。

很明显,在这2个场景中,小王的工作时间更加紧凑,特别是那些排队的时间都可以让外卖员去干,因此可以更加专注于自己的本职工作。聪明的你应该也已经体会到了,场景1就是典型的函数同步调用,而场景2是典型的异步调用。

而场景2的异步调用,还有一个特点,就是它拥有一个返回值,这个返回值就是我们的订单。这个订单很重要,凭借着这个订单,我们才能够取得当前这个调用所对应的结果。

这里的订单就如同Future模式中的Future,这是一个合约,一份承诺。虽然订单不能吃,但是手握订单,不怕没吃的,虽然Future不是我们想要的结果,但是拿着Future就能在将来得到我们想要的结果。

因此,Future模式很好的解决了那些需要返回值的异步调用。

Future模式中的主要角色

一个典型的Future模式由以下几个部分组成:

  • Main:系统启动,调用Client发出请求
  • Client:返回Data对象,立即返回FutureData,并开启ClientThread线程装配RealData
  • Data:返回数据的接口
  • FutureData:Future数据,构造很快,但是是一个虚拟的数据,需要装配RealData,好比一个订单
  • RealData:真实数据,其构造是比较慢的,好比上面例子中的肯德基午餐。

它们之间的相互关系如下图:

其中,值得注意是Data,RealData和FutureData。这是一组典型的代理模式,Data接口表示对外数据,RealData表示真实的数据,就好比午餐,获得它的成本比较高,需要很多时间;相对的FutureData作为RealData的代理,类似于一个订单/契约,通过FutureData,可以在将来获得RealData。

因此,Future模式本质上是代理模式的一种实际应用。

实现一个简单的Future模式

根据上面的设计,让我们来实现一个简单的代理模式吧!

首先是Data接口,代表数据:

  1. public interface Data { 
  2.     public String getResult (); 

接着是FutureData,也是整个Future模式的核心:

  1. public class FutureData implements Data { 
  2.     // 内部需要维护RealData 
  3.     protected RealData realdata = null;           
  4.     protected boolean isReady = false
  5.     public synchronized void setRealData(RealData realdata) { 
  6.         if (isReady) {  
  7.             return
  8.         } 
  9.         this.realdata = realdata; 
  10.         isReady = true
  11.         //RealData已经被注入,通知getResult() 
  12.         notifyAll();                                
  13.     } 
  14.     //会等待RealData构造完成 
  15.     public synchronized String getResult() {          
  16.         while (!isReady) { 
  17.             try { 
  18.                 //一直等待,直到RealData被注入 
  19.                 wait();                            
  20.             } catch (InterruptedException e) { 
  21.             } 
  22.         } 
  23.         //真正需要的数据从RealData获取 
  24.         return realdata.result;                       
  25.     } 

下面是RealData:

  1. public class RealData implements Data { 
  2.     protected final String result; 
  3.     public RealData(String para) { 
  4.         StringBuffer sb=new StringBuffer(); 
  5.         //假设这里很慢很慢,构造RealData不是一个容易的事 
  6.         result =sb.toString(); 
  7.     } 
  8.     public String getResult() { 
  9.         return result; 
  10.     } 

然后从Client得到Data:

  1. public class Client { 
  2.     //这是一个异步方法,返回的Data接口是一个Future 
  3.     public Data request(final String queryStr) { 
  4.         final FutureData future = new FutureData(); 
  5.         new Thread() {                                       
  6.             public void run() {                      
  7.                 // RealData的构建很慢,所以在单独的线程中进行 
  8.                 RealData realdata = new RealData(queryStr); 
  9.                 //setRealData()的时候会notify()等待在这个future上的对象 
  10.                 future.setRealData(realdata); 
  11.             }                                                
  12.         }.start(); 
  13.         // FutureData会被立即返回,不会等待RealData被构造完 
  14.         return future;                           
  15.     } 

最后一个Main函数,把所有一切都串起来:

  1. public static void main(String[] args) { 
  2.     Client client = new Client(); 
  3.     //这里会立即返回,因为得到的是FutureData而不是RealData 
  4.     Data data = client.request("name"); 
  5.     System.out.println("请求完毕"); 
  6.     try { 
  7.         //这里可以用一个sleep代替了对其他业务逻辑的处理 
  8.         //在处理这些业务逻辑的过程中,RealData被创建,从而充分利用了等待时间 
  9.         Thread.sleep(2000); 
  10.     } catch (InterruptedException e) { 
  11.     } 
  12.     //使用真实的数据,如果到这里数据还没有准备好,getResult()会等待数据准备完,再返回 
  13.     System.out.println("数据 = " + data.getResult()); 

这是一个最简单的Future模式的实现,虽然简单,但是已经包含了Future模式中最精髓的部分。对大家理解JDK内部的Future对象,有着非常重要的作用。

Java中的Future模式

Future模式是如此常用,在JDK内部已经有了比较全面的实现和支持。下面,让我们一起看看JDK内部的Future实现:

首先,JDK内部有一个Future接口,这就是类似前面提到的订单,当然了,作为一个完整的商业化产品,这里的Future的功能更加丰富了,除了get()方法来获得真实数据以外,还提供一组辅助方法,比如:

  • cancel():如果等太久,你可以直接取消这个任务
  • isCancelled():任务是不是已经取消了
  • isDone():任务是不是已经完成了
  • get():有2个get()方法,不带参数的表示无穷等待,或者你可以只等待给定时间

下面代码演示了这个Future的使用方法:

  1. //异步操作 可以用一个线程池 
  2.   ExecutorService executor = Executors.newFixedThreadPool(1); 
  3.   //执行FutureTask,相当于上例中的 client.request("name") 发送请求 
  4.   //在这里开启线程进行RealData的call()执行 
  5.   Future<String> future = executor.submit(new RealData("name")); 
  6.   System.out.println("请求完毕,数据准备中"); 
  7.   try { 
  8.       //这里依然可以做额外的数据操作,这里使用sleep代替其他业务逻辑的处理 
  9.       Thread.sleep(2000); 
  10.   } catch (InterruptedException e) { 
  11.   } 
  12.   //如果此时call()方法没有执行完成,则依然会等待 
  13.   System.out.println("数据 = " + future.get()); 

整个使用过程非常简单,下面我们来分析一下executor.submit()里面究竟发生了什么:

  1. public <T> Future<T> submit(Callable<T> task) { 
  2.     if (task == null) throw new NullPointerException(); 
  3.     // 根据Callable对象,创建一个RunnableFuture,这里其实就是FutureTask 
  4.     RunnableFuture<T> ftask = newTaskFor(task); 
  5.     //将ftask推送到线程池 
  6.     //在新线程中执行的,就是run()方法,在下面的代码中有给出 
  7.     execute(ftask); 
  8.     //返回这个Future,将来通过这个Future就可以得到执行的结果 
  9.     return ftask; 
  10. protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { 
  11.     return new FutureTask<T>(callable); 

最关键的部分在下面,FutureTask作为一个线程单独执行时,会将结果保存到outcome中,并设置任务的状态,下面是FutureTask的run()方法:

从FutureTask中获得结果的实现如下:

  1. public V get() throws InterruptedException, ExecutionException { 
  2.        int s = state; 
  3.        //如果没有完成,就等待,回到用park()方法阻塞线程 
  4.        //同时,所有等待线程会在FutureTask的waiters字段中排队等待 
  5.        if (s <= COMPLETING) 
  6.            s = awaitDone(false, 0L); 
  7.        return report(s); 
  8.    } 
  9.    private V report(int s) throws ExecutionException { 
  10.        //outcome里保存的就是最终的计算结果 
  11.        Object x = outcome; 
  12.        if (s == NORMAL) 
  13.            //正常完成,就返回outcome 
  14.            return (V)x; 
  15.        //如果没有正常完成, 比如被用户取消了,或者有异常了,就抛出异常 
  16.        if (s >= CANCELLED) 
  17.            throw new CancellationException(); 
  18.        throw new ExecutionException((Throwable)x); 
  19.    } 

Future模式的高阶版本—— CompletableFuture

Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行完,如果执行调用get()方法或者isDone()方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。

为了解决这个问题,JDK对Future模式又进行了加强,创建了一个CompletableFuture,它可以理解为Future模式的升级版本,它最大的作用是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。

下面来看一个简单的例子:

在这个例子中,首先以getPrice()为基础创建一个异步调用,接着,使用thenAccept()方法,设置了一个后续的操作,也就是当getPrice()执行完成后的后续处理。

不难看到,CompletableFuture比一般的Future更具有实用性,因为它可以在Future执行成功后,自动回调进行下一步的操作,因此整个程序不会有任何阻塞的地方(也就是说你不用去到处等待Future的执行,而是让Future执行成功后,自动来告诉你)。

以上面的代码为例,CompletableFuture之所有会有那么神奇的功能,完全得益于AsyncSupply类(由上述代码中的supplyAsync()方法创建)。

AsyncSupply在执行时,如下所示:

  1. public void run() { 
  2.             CompletableFuture<T> d; Supplier<T> f; 
  3.             if ((d = dep) != null && (f = fn) != null) { 
  4.                 dep = null; fn = null
  5.                 if (d.result == null) { 
  6.                     try { 
  7.                         //这里就是你要执行的异步方法 
  8.                         //结果会被保存下来,放到d.result字段中 
  9.                         d.completeValue(f.get()); 
  10.                     } catch (Throwable ex) { 
  11.                         d.completeThrowable(ex); 
  12.                     } 
  13.                 } 
  14.                 //执行成功了,进行后续处理,在这个后续处理中,就会调用thenAccept()中的消费者 
  15.                 //这里就相当于Future完成后的通知 
  16.                 d.postComplete(); 
  17.             } 
  18.         } 

继续看d.postComplete(),这里会调用后续一系列操作

  1. final void postComplete() { 
  2.              //省略部分代码,重点在tryFire()里 
  3.              //在tryFire()里,真正触发了后续的调用,也就是thenAccept()中的部分 
  4.              f = (d = h.tryFire(NESTED)) == null ? this : d; 
  5.          } 
  6.      } 
  7.  } 

絮叨

今天,我们主要介绍Future模式,我们从一个最简单的Future模式开始,逐步深入,先后介绍了JDK内部的Future模式实现,以及对Future模式的进化版本CompletableFuture做了简单的介绍。对

于多线程开发而言,Future模式的应用极其广泛,可以说这个模式已经成为了异步开发的基础设施。

 

责任编辑:武晓燕 来源: 三太子敖丙
相关推荐

2022-07-29 11:19:38

日志框架实践

2010-11-02 14:51:11

职场

2020-07-09 12:50:29

JVM内存管理Java

2019-10-18 17:55:03

安全运营

2019-12-24 14:04:59

PythonExcel数据处理

2020-06-15 08:19:00

ZooKeeperEureka

2011-03-31 10:46:54

LinuxCLI软件

2019-11-14 15:38:46

AndroidRelease项目

2010-05-26 15:58:52

MySQL远程连接

2010-05-21 09:40:57

MySQL出错代码列表

2010-05-10 13:01:03

OracleDBA面试

2010-05-25 09:58:43

MySQL数据库

2010-05-26 15:17:06

Windows Emb

2018-08-06 11:59:00

混合云数据中心上云

2011-04-26 09:44:05

Power Cloud

2014-10-30 13:38:55

编程算法程序员

2011-09-23 09:24:26

豆瓣电台应用

2015-08-31 14:12:12

DockerKubernetesPaaS

2017-08-16 18:03:12

Docker安全工具容器

2019-12-10 15:30:27

SaaSIaaS云计算
点赞
收藏

51CTO技术栈公众号