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. MIP 问题解决方案大全(2018-06更新)

    在 MIP 推出后,我们收到了一些站长的疑问.现将常见问题整理出来,帮助大家了解 MIP 的知识. 一.MIP 认知类问题 二.改造前准备 三.前端改造,组件使用 四.提交生效 五.MIPCache ...

  2. 死磕 java集合之ConcurrentLinkedQueue源码分析

    问题 (1)ConcurrentLinkedQueue是阻塞队列吗? (2)ConcurrentLinkedQueue如何保证并发安全? (3)ConcurrentLinkedQueue能用于线程池吗 ...

  3. Asp.NetCore轻松学-部署到 Linux 进行托管

    前言 上一篇文章介绍了如何将开发好的 Asp.Net Core 应用程序部署到 IIS,且学习了进程内托管和进程外托管的区别:接下来就要说说应用 Asp.Net Core 的特性(跨平台),将 .Ne ...

  4. java之servlet入门操作教程一续

    本节主要是在java之servlet入门操作教程一  的基础上使用myeclipse实现自动部署的功能 准备: java之servlet入门操作教程一 中完成myFirstServlet项目的创建: ...

  5. 面向对象OOP概念描述

    面向对象三大特征:封装,继承,多态 封装就是把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法 继承就是在已经存在的类的定义作为基础,建立新的技术.新定义的类可以添加新的数据或功能,也可以 ...

  6. 常见文本类css属性

    学习web的第六天 p{ font-size:18px;  /*文本尺寸*/ font-family:"隶书";  /*文本字体*/ font-weight:bold;    /* ...

  7. [目录]搭建一个简单的WebGIS应用程序

    “如果一件事情超过自己的能力,自己很难达到,那就像是婴儿跳高,不但没有好处,反而拔苗助长”. 4月份时报名参加了2018年ESRI杯GIS应用开发比赛,到前几天提交了作品.作品很简单,没有那么多复杂深 ...

  8. python检测字符串乱码

    import chardet f=open('test.txt','rb') f_read=f.read() f_charInfo=chardet.detect(f_read) print(f_cha ...

  9. Node.js 事件循环机制

    Node.js 采用事件驱动和异步 I/O 的方式,实现了一个单线程.高并发的 JavaScript 运行时环境,而单线程就意味着同一时间只能做一件事,那么 Node.js 如何通过单线程来实现高并发 ...

  10. 抽象类(abstract class)与接口(interface)的异同

    抽象类:如果一个类中包含抽象方法,那么这个类就是抽象类.在Java语言中,可以通过把类或类中的某些方法声明为abstract(abstract只能修饰类或方法,不能修饰属性)来表示一个类是抽象类. 接 ...