1.1、Future模式是什么

先简单举个例子介绍,当我们平时写一个函数,函数里的语句一行行同步执行,如果某一行执行很慢,程序就必须等待,直到执行结束才返回结果;但有时我们可能并不急着需要其中某行的执行结果,想让被调用者立即返回。比如小明在某网站上成功创建了一个账号,创建完账号后会有邮件通知,如果在邮件通知时因某种原因耗时很久(此时账号已成功创建),使用传统同步执行的方式那就要等完这个时间才会有创建成功的结果返回到前端,但此时账号创建成功后我们并不需要立即关心邮件发送成功了没,此时就可以使用Future模式,让安在后台慢慢处理这个请求,对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合(比如某时想要知道邮件发送是否成功)再去尝试获取需要的数据。

使用Future模式,获取数据的时候可能无法立即得到需要的数据。而是先拿到一个包装,可以在需要的时候再去get获取需要的数据。

1.2、Future模式与传统模式的区别

先看看请求返回的时序图,明显传统的模式是串行同步执行的,在遇到耗时操作的时候只能等待。反观Future模式,发起一个耗时操作后,函数会立刻返回,并不会阻塞客户端线程。所以在执行实际耗时操作时候客户端无需等待,可以做其他事情,直到需要的时候再向工作线程获取结果。

2.1、动手实现简易Future模式

下面的DataFuture类只是一个包装类,创建它时无需阻塞等待。在工作线程准备好数据后使用setRealData方法将数据传入。客户端只要在真正需要数据时调用getRealData方法即可,如果此时数据已准备好则立即返回,否则getRealData方法就会等待,直到获取数据完成。

public class DataFuture<T> {
private T realData;
private boolean isOK = false; public synchronized T getRealData() {
while (!isOK) {
try {
// 数据未准备好则等待
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
return realData;
} public synchronized void setRealData(T data) {
isOK = true;
realData = data;
notifyAll();
}
}

下面实现一服务端,客户端向服务端请求数据时,服务端并不会立刻去加载真正数据,只是创建一个DataFuture,创建子线程去加载真正数据,服务端直接返回DataFuture即可。

public class Server {

    public DataFuture<String> getData() {
final DataFuture<String> data = new DataFuture<>();
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setRealData("最终数据");
}
});
return data;
}
}

最终客户端调用 代码如下:

long start = System.currentTimeMillis();
Server server = new Server();
DataFuture<String> dataFuture = server.getData(); try {
// 先执行其他操作
Thread.sleep(5000);
// 模拟耗时...
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.print("结果数据:" + dataFuture.getRealData());
System.out.println("耗时: " + (System.currentTimeMillis() - start));

结果:

结果数据:最终数据
耗时: 5021

执行最终数据耗时都在5秒左右,如果串行执行的话就是10秒左右。

2.2、JDK中的Future与FutureTask

先来看看Future接口源码:

public interface Future<V> {
/**
* 用来取消任务,取消成功则返回true,取消失败则返回false。
* mayInterruptIfRunning参数表示是否允许取消正在执行却没有执行完毕的任务,设为true,则表示可以取消正在执行过程中的任务。
* 如果任务已完成,则无论mayInterruptIfRunning为true还是false,此方法都返回false,即如果取消已经完成的任务会返回false;
* 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
* 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
*/
boolean cancel(boolean mayInterruptIfRunning); /**
* 表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true
*/
boolean isCancelled(); /**
* 表示任务是否已经完成,若任务完成,则返回true
*/
boolean isDone(); /**
* 获取执行结果,如果最终结果还没得出该方法会产生阻塞,直到任务执行完毕返回结果
*/
V get() throws InterruptedException, ExecutionException; /**
* 获取执行结果,如果在指定时间内,还没获取到结果,则抛出TimeoutException
*/
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

从上面源码可看出Future就是对于Runnable或Callable任务的执行进行查询、中断任务、获取结果。下面就以一个计算1到1亿的和为例子,看使用传统方式和使用Future耗时差多少。先看传统方式代码:

public class FutureTest {

    public static void main(String[] args) {
long start = System.currentTimeMillis();
List<Integer> retList = new ArrayList<>(); // 计算1000次1至1亿的和
for (int i = 0; i < 1000; i++) {
retList.add(Calc.cal(100000000));
}
System.out.println("耗时: " + (System.currentTimeMillis() - start)); for (int i = 0; i < 1000; i++) {
try {
Integer result = retList.get(i);
System.out.println("第" + i + "个结果: " + result);
} catch (Exception e) {
}
}
System.out.println("耗时: " + (System.currentTimeMillis() - start));
} public static class Calc implements Callable<Integer> { @Override
public Integer call() throws Exception {
return cal(10000);
} public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
return sum;
}
}
}

执行结果(耗时40+秒):

耗时: 43659
第0个结果: 887459712
第1个结果: 887459712
第2个结果: 887459712
...
第999个结果: 887459712
耗时: 43688

再来看看使用Future模式下程序:

public class FutureTest {

    public static void main(String[] args) {
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<Integer>> futureList = new ArrayList<>(); // 计算1000次1至1亿的和
for (int i = 0; i < 1000; i++) {
// 调度执行
futureList.add(executorService.submit(new Calc()));
}
System.out.println("耗时: " + (System.currentTimeMillis() - start)); for (int i = 0; i < 1000; i++) {
try {
Integer result = futureList.get(i).get();
System.out.println("第" + i + "个结果: " + result);
} catch (InterruptedException | ExecutionException e) {
}
}
System.out.println("耗时: " + (System.currentTimeMillis() - start));
} public static class Calc implements Callable<Integer> { @Override
public Integer call() throws Exception {
return cal(100000000);
} public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
return sum;
}
}
}

执行结果(耗时12+秒):

耗时: 12058
第0个结果: 887459712
第1个结果: 887459712
...
第999个结果: 887459712
耗时: 12405

可以看到,计算1000次1至1亿的和,使用Future模式并发执行最终的耗时比使用传统的方式快了30秒左右,使用Future模式的效率大大提高。

2.3、FutureTask

说完Future,Future因为是接口不能直接用来创建对象,就有了下面的FutureTask。

先看看FutureTask的实现:

public class FutureTask<V> implements RunnableFuture<V>

可以看到FutureTask类实现了RunnableFuture接口,接着看RunnableFuture接口源码:

public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}

可以看到RunnableFuture接口继承了Runnable接口和Future接口,也就是说其实FutureTask既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。

看下面FutureTask的两个构造方法,可以看出就是为这两个操作准备的。

public FutureTask(Callable<V> var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
this.callable = var1;
this.state = 0;
}
} public FutureTask(Runnable var1, V var2) {
this.callable = Executors.callable(var1, var2);
this.state = 0;
}

FutureTask使用实例:

public class FutureTest {

    public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Calc task = new Calc();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();
} public static class Calc implements Callable<Integer> { @Override
public Integer call() throws Exception {
return cal(100000000);
} public static int cal (int num) {
int sum = 0;
for (int i = 0; i < num; i++) {
sum += i;
}
return sum;
}
}
}

2.4、Future不足之处

上面例子可以看到使用Future模式比传统模式效率明显提高了,使用Future一定程度上可以让一个线程池内的任务异步执行;但同时也有个明显的缺点:就是回调无法放到与任务不同的线程中执行,传统回调最大的问题就是不能将控制流分离到不同的事件处理器中。比如主线程要等各个异步执行线程返回的结果来做下一步操作,就必须阻塞在future.get()方法等待结果返回,这时其实又是同步了,如果遇到某个线程执行时间太长时,那情况就更糟了。

到Java8时引入了一个新的实现类CompletableFuture,弥补了上面的缺点,在下篇会讲解CompletableFuture的使用。

作者注:原文发表在公号(点击查看),定期分享IT互联网、金融等工作经验心得、人生感悟,欢迎订阅交流,目前就职阿里-移动事业部,需要大厂内推的也可到公号砸简历。(公众号ID:weknow619)

【并发编程】Future模式及JDK中的实现的更多相关文章

  1. java并发编程系列原理篇--JDK中的通信工具类Semaphore

    前言 java多线程之间进行通信时,JDK主要提供了以下几种通信工具类.主要有Semaphore.CountDownLatch.CyclicBarrier.exchanger.Phaser这几个通讯类 ...

  2. 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理

    (1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...

  3. 《 .NET并发编程实战》一书中的节流为什么不翻译成限流

    有读者问,为什么< .NET并发编程实战>一书中的节流为什么不翻译成限流? 这个问题问得十分好!毕竟“限流”这个词名气很大,耳熟能详,知名度比“节流”大多了. 首先,节流的原词Thrott ...

  4. Java并发编程(十)-- Java中的锁

    在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁.非公平锁.自旋锁.可重入锁.偏向锁.轻量级锁.重量级锁.读写锁.互斥锁.死锁.活锁等,本文将简概的介绍一下各种锁. 公平锁和非公平锁 ...

  5. 并发编程-Future+callable+FutureTask 闭锁机制

    项目中经常有些任务需要异步(提交到线程池中)去执行,而主线程往往需要知道异步执行产生的结果,这时我们要怎么做呢?用runnable是无法实现的,我们需要用callable实现. FutureTask ...

  6. Java并发编程(十一)-- Java中的锁详解

    上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...

  7. 并发编程(二)Java中的多线程

    一.创建多线程 创建多线程有以下几种方法: 继承Thread,重写run方法 实现Runnable接口,重写run方法[无返回值] 实现Callable接口,重写call方法[有返回值] 继承Thre ...

  8. Java并发编程:Future接口、FutureTask类

    在前面的文章中我们讲述了创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口. 这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果. 如果需要获取执行结果,就 ...

  9. 《Java并发编程的艺术》--Java中的锁

    No1: Lock接口 Lock lock = new ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); } No2: 不要讲获取 ...

随机推荐

  1. Text-CNN-文本分类-keras

    Text CNN 1. 简介 TextCNN 是利用卷积神经网络对文本进行分类的算法,由 Yoon Kim 在 "Convolutional Neural Networks for Sent ...

  2. Java编程思想 - 并发

    前言 Q: 为什么学习并发? A: 到目前为止,你学到的都是有关顺序编程的知识,即程序中的所有事物在任意时刻都只能执行一个步骤. A: 编程问题中相当大的一部分都可以通过使用顺序编程来解决,然而,对于 ...

  3. numpy C语言源代码调试(三)

    鉴于ddd过于简陋,希望找一个新一些的调试工具,看到有很多人推荐gdbgui,这是一个非常新的调试工具,前端使用浏览器,现在采用这一架构的软件越来越多,可以完全不必依赖庞大的gui类库,安装使用比较方 ...

  4. C#-Xamarin的Activity传值与Fragment引用

    前言 我们学习任何一个新框架时,肯定都需要学习它的子页面用法,因为子页面是封装公共内容最好的容器. 在Xamarin里子页面为Fragment,翻译过来是片段的意思. Fragment 下面我们来学习 ...

  5. @GeneratedValue源码解析

    JPA要求每一个实体必须有且只有一个主键,而@GeneratedValue提供了主键的生成策略,这就是@GeneratedValue注解存在的意义.本文将浅析@GeneratedValue的源码. @ ...

  6. ASP.NET三层架构项目创建流程

    1.进入VS2010,新建项目—>Visual C#—>Web—>ASP.NET空Web应用程序,如图所示: 2.在解决方案处右击—>新建项目—>Windows—> ...

  7. 兼容性:Adapter(适配器模式)【PHP】

    Adapter(适配器模式) ---- 加个“适配器”以便于复用 将一个类的接口转换成客户希望的另一个接口.Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. 应用场景 如果 ...

  8. 通过免费开源ERP构建业界领先的供应链+垂直电商平台成功案例分享

    案例客户简介 Healey Green是一家新成立的企业,在线销售和销售园艺机械. 他们的产品范围包括草坪割草机,割灌机,地钻,链锯等. 在一个竞争非常激烈的市场中,这位雄心勃勃的新人将开始接受那些以 ...

  9. AI2(App Inventor 2)离线版服务器单机版

    注意:每次退出前导出自己的项目到本地做备份. 单机版特点: 1.同步官方最新版本,没有对java源代码进行修改,仅修改war\login.jsp及\war\WEB-INF\appengine-web. ...

  10. PostgreSQL 10.7 linux 主从配置

    PostgreSQL 10.7 主从安装 硬件环境 云服务商:华为云 Linux: CentOS7.1 工具:Xshell Xftp IP:114.115.251.168 Port: 5432 543 ...