我们为什么要学习Future模式

大型分布式系统当中,利用Future模式来解决高并发的问题是相当常见的,举几个例子:

1.Guava中的SettableFuture,Spring中的ListenableFuture(Guava中也有),我们可以实现自动监控异步任务的运行结果并异步处理结果。从而提高CPU的利用率,有效得完成系统任务的调度。

2.Dubbo中的异步实现

java future获取返回值如何实现 java future get原理_类关系

dubbo异步调用原理

Dubbo的异步实现中,IO线程与Server的交互上,采用的是NIO非阻塞实现并行调度,客户端不需要启动多线程即可调用多个远程服务,相对线程开销较小。用户线程与IO线程的交互中,采用的是Future模式,Dubbo同步的实现实际上内部就是调用了Future.get()方法,而异步模式这是将Future对象返给用户,由用户决定什么实用get返回值。

以上例子可以看出,在高吞吐量,高并发的场景,Future模式是必不可少的,可以大大减少同步调用带来的服务器资源占用,从而提高系统的利用率。

学习Future模式是高并发场景中的基础,对于理解高并发场景下的多线程设计与实现很有帮助。

Future与Callable

Java线程池提供了两种可执行的对象,一种是Callable,另一种是Runnable,线程池方法如下:

public Future submit(Runnable task, T result) -- AbstractExecutorService
public Future submit(Callable task) -- AbstractExecutorService
public void execute(Runnable command) -- ThreadPoolExecutor

类关系是:ThreadPoolExecutor extends AbstractExecutorService

可以看出,当我们调用submit的时候,就可以拿到一个Future对象,如果调用execute方法的时候,则不会有任何的异步返回值。

当调用submit(Runnable task)方法的时候,由于Runnable中的run方法没有返回值,所以Future会返回null,但是可以从外部感知到任务已经完成。

当我们的异步任务是有返回的Callable,线程池通过Future将异步任务的结果返给调用者。

Future模式的源码实现:FutureTask

run方法的实现

当调用submit的时候,返回的Future接口指向了其一个实现FutureTask,其类关系如下:

java future获取返回值如何实现 java future get原理_类关系_02

FutureTask类关系

其中的核心方法在上图中都有所体现,get()方法在调用时,如果run()方法没有执行完,会通过for(;;) + park阻塞线程直到拿到run()方法执行的结果。

当调用submit方法的时候,由于包装的FutureTask实现了Runnable,所以可以直接调用execute方法执行,其源码如下:

java future获取返回值如何实现 java future get原理_高并发_03

submit方法的实现

可以看出,实际上FutureTask代理了task对象,属于静态代理模式。

其中的run方法的实现

java future获取返回值如何实现 java future get原理_类关系_04

run方法的实现

这里的run即调用其代理对象Callable中的call方法拿到结果,然后通过set方法将值复制到outcome中,这里采用unsafe实现线程占用字段runner的方式来保证任务不被重复执行。

java future获取返回值如何实现 java future get原理_高并发_05

set方法的实现

可以看出整个过程是线程安全的,采用的是unsafe来改变Future对象的状态。也就是说需要拿到call方法的结果之后,new的状态才会转换到completing(完成)状态,最终转换成normal状态

get方法的实现

java future获取返回值如何实现 java future get原理_pthread异步_06

get方法的实现

首先还是状态的判断,如果状态<=完成状态,就会调用awaitDone,否则直接返回

awaitDone方法实现如下:

java future获取返回值如何实现 java future get原理_高并发_07

awaitDone方法的实现

这个方法有点长,核心方法已经标记,可以说是通过循环+状态判断来实现等待结果返回的,之前的run方法我们已经知道如果是完成状态,会立刻被改成nomal状态,所以这里按断了如果是完成状态,调用Thread.yield()方法建议cpu调度当前线程,并且等待过程中会创建等待节点WaitNode,这个等待节点的数据结构如下:

java future获取返回值如何实现 java future get原理_异步任务_08

WaitNode等待节点的实现

保存的是等待的线程,并且是一个链表节点的设计,这里的unsafe则实现了将等待线程排成一个链表,并且是头结点插入模式,后面来的线程都会打入到头结点。park方法即进入阻塞状态。

与Object类的wait/notify机制相比,park/unpark有两个优点:

以thread为操作对象更符合阻塞线程的直观定义

操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

这里进入阻塞,最后当然是由set方法调用的finishCompletion来唤醒的。其源码如下:

java future获取返回值如何实现 java future get原理_pthread异步_09

finish方法的实现

可以看出,同样是通过unsafe来实现只有一个线程能够执行释放信号量的过程,这里通过unpark来赋值线程信号量,从而唤醒线程,由于这个链表使后来者放在头部,所以越后面做等待的线程,越早被释放掉,也就越早拿到返回值。

这里park底层是通过pthread_cond_wait()方法来实现线程阻塞的,该方法为linux底层的方法,比较难理解