Future是我们在使用java实现异步时最常用到的一个类,我们可以向线程池提交一个Callable,并通过future对象获取执行结果。本篇文章主要讲述了JUC中FutureTask中的一些实现原理。使用的jdk版本是1.7。

Future

  Future是一个接口,它定义了5个方法:

boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; 

简单说明一下接口定义

  • boolean cancel(boolean mayInterruptInRunning) 取消一个任务,并返回取消结果。参数表示是否中断线程。
  • boolean isCancelled()                                        判断任务是否被取消
  • Boolean isDone()                                          判断当前任务是否执行完毕,包括正常执行完毕、执行异常或者任务取消。
  • V get()                                                            获取任务执行结果,任务结束之前会阻塞。
  • V get(long timeout, TimeUnit unit)                    在指定时间内尝试获取执行结果。若超时则抛出超时异常

写个简单demo:

public class FutureDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Future future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
Long start = System.currentTimeMillis();
while (true) {
Long current = System.currentTimeMillis();
if ((current - start) > 1000) {
return 1;
}
}
}
}); try {
Integer result = (Integer)future.get();
System.out.println(result);
}catch (Exception e){
e.printStackTrace();
}
}
} 

 这里模拟了1s钟的CPU空转,当执行future.get()的时候,主线程阻塞了大约一秒后获得结果。

当然我们也可以使用get(long timeout, TimeUnit unit)

try {
Integer result = (Integer) future.get(500, TimeUnit.MILLISECONDS);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
} 

由于在500ms内没有结果返回,所以抛出异常,打印异常堆栈如下

当然,如果我们把超时时间设置的长一些,还是可以得到预期的结果的。

FutureTask实现原理

下面我们介绍一下FutureTask内部的一些实现机制。下文从以下几点叙述:

  1. 类继承结构
  2. 核心成员变量
  3. 内部状态转换
  4. 核心方法解析

1 类继承结构

首先我们看一下FutureTask的继承结构:

   

FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future,也就是说FutureTask既是Runnable,也是Future。

2 核心成员变量

FutureTask内部定义了以下变量,以及它们的含义如下

  • volatile int state:表示对象状态,volatile关键字保证了内存可见性。futureTask中定义了7种状态,代表了7种不同的执行状态
private static final int NEW          = 0; //任务新建和执行中
private static final int COMPLETING = 1; //任务将要执行完毕
private static final int NORMAL = 2; //任务正常执行结束
private static final int EXCEPTIONAL = 3; //任务异常
private static final int CANCELLED = 4; //任务取消
private static final int INTERRUPTING = 5; //任务线程即将被中断
private static final int INTERRUPTED = 6; //任务线程已中断
  • Callable<V> callable:被提交的任务
  • Object outcome:任务执行结果或者任务异常
  • volatile Thread runner:执行任务的线程
  • volatile WaitNode waiters:等待节点,关联等待线程
  • long stateOffset:state字段的内存偏移量
  • long runnerOffset:runner字段的内存偏移量
  • long waitersOffset:waiters字段的内存偏移量

后三个字段是配合Unsafe类做CAS操作使用的。

3 内部状态转换

FutureTask中使用state表示任务状态,state值变更的由CAS操作保证原子性。

FutureTask对象初始化时,在构造器中把state置为为NEW,之后状态的变更依据具体执行情况来定。

   例如任务执行正常结束前,state会被设置成COMPLETING,代表任务即将完成,接下来很快就会被设置为NARMAL或者EXCEPTIONAL,这取决于调用Runnable中的call()方法是否抛出了异常。有异常则后者,反之前者。

  任务提交后、任务结束前取消任务,那么有可能变为CANCELLED或者INTERRUPTED。在调用cancel方法时,如果传入false表示不中断线程,state会被置为CANCELLED,反之state先被变为INTERRUPTING,后变为INTERRUPTED。

总结下,FutureTask的状态流转过程,可以出现以下四种情况:

1. 任务正常执行并返回。 NEW -> COMPLETING -> NORMAL

    2. 执行中出现异常。NEW -> COMPLETING -> EXCEPTIONAL

3. 任务执行过程中被取消,并且不响应中断。NEW -> CANCELLED

    4. 任务执行过程中被取消,并且响应中断。 NEW -> INTERRUPTING -> INTERRUPTED  

4 核心方法解析

  接下来我们一起扒一扒FutureTask的源码。我们先看一下任务线程是怎么执行的。当任务被提交到线程池后,会执行futureTask的run()方法。

1 public void run()

public void run() {
// 校验任务状态
if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
       // double check
if (c != null && state == NEW) {
V result;
boolean ran;
try {
            //执行业务代码
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
       // 重置runner
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

  翻译一下,这个方法经历了以下几步

  1. 校验当前任务状态是否为NEW以及runner是否已赋值。这一步是防止任务被取消。
  2. double-check任务状态state
  3. 执行业务逻辑,也就是c.call()方法被执行
  4. 如果业务逻辑异常,则调用setException方法将异常对象赋给outcome,并且更新state值
  5. 如果业务正常,则调用set方法将执行结果赋给outcome,并且更新state值

我们继续往下看,setException(Throwable t)和set(V v) 具体是怎么做的

protected void set(V v) {
// state状态 NEW->COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
// COMPLETING -> NORMAL 到达稳定状态
UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
// 一些结束工作
finishCompletion();
}
}
protected void setException(Throwable t) {
// state状态 NEW->COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
// COMPLETING -> EXCEPTIONAL 到达稳定状态
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
// 一些结束工作
finishCompletion();
}
} 

code中的注释已经写的很清楚,故不翻译了。状态变更的原子性由unsafe对象提供的CAS操作保证。FutureTask的outcome变量存储执行结果或者异常对象,会由主线程返回。

2  get()和get(long timeout, TimeUnit unit)

任务由线程池提供的线程执行,那么这时候主线程则会阻塞,直到任务线程唤醒它们。我们通过get(long timeout, TimeUnit unit)方法看看是怎么做的  

public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}

   get的源码很简洁,首先校验参数,然后根据state状态判断是否超时,如果超时则异常,不超时则调用report(s)去获取最终结果。

当 s<= COMPLETING时,表明任务仍然在执行且没有被取消。如果它为true,那么走到awaitDone方法。

awaitDone是futureTask实现阻塞的关键方法,我们重点关注一下它的实现原理。

     /**
* 等待任务执行完毕,如果任务取消或者超时则停止
* @param timed 为true表示设置超时时间
* @param nanos 超时时间
* @return 任务完成时的状态
* @throws InterruptedException
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 任务截止时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
// 自旋
for (;;) {
if (Thread.interrupted()) {
//线程中断则移除等待线程,并抛出异常
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) {
// 任务可能已经完成或者被取消了
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING)
// 可能任务线程被阻塞了,主线程让出CPU
Thread.yield();
else if (q == null)
// 等待线程节点为空,则初始化新节点并关联当前线程
q = new WaitNode();
else if (!queued)
// 等待线程入队列,成功则queued=true
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
//已经超时的话,移除等待节点
removeWaiter(q);
return state;
}
// 未超时,将当前线程挂起指定时间
LockSupport.parkNanos(this, nanos);
}
else
// timed=false时会走到这里,挂起当前线程
LockSupport.park(this);
}
}

注释里也很清楚的写明了每一步的作用,我们以设置超时时间为例,总结一下过程

  1. 计算deadline,也就是到某个时间点后如果还没有返回结果,那么就超时了。
  2. 进入自旋,也就是死循环。
  3. 首先判断是否响应线程中断。对于线程中断的响应往往会放在线程进入阻塞之前,这里也印证了这一点。
  4. 判断state值,如果>COMPLETING表明任务已经取消或者已经执行完毕,就可以直接返回了。
  5. 如果任务还在执行,则为当前线程初始化一个等待节点WaitNode,入等待队列。这里和AQS的等待队列类似,只不过Node只关联线程,而没有状态。AQS里面的等待节点是有状态的。
  6. 计算nanos,判断是否已经超时。如果已经超时,则移除所有等待节点,直接返回state。超时的话,state的值仍然还是COMPLETING。
  7. 如果还未超时,就通过LockSupprot类提供的方法在指定时间内挂起当前线程,等待任务线程唤醒或者超时唤醒。

当线程被挂起之后,如果任务线程执行完毕,就会唤醒等待线程哦。这一步就是在finishCompletion里面做的,前面已经提到这个方法。我们再看看这个方法具体做了哪些事吧~

    /**
* 移除并唤醒所有等待线程,执行done,置空callable
* nulls out callable.
*/
private void finishCompletion() {
//遍历等待节点
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
//唤醒等待线程
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
// unlink to help gc
q.next = null;
q = next;
}
break;
}
}
//模板方法,可以被覆盖
done();
//清空callable
callable = null;
}

由代码和注释可以看出来,这个方法的作用主要在于唤醒等待线程。由前文可知,当任务正常结束或者异常时,都会调用finishCompletion去唤醒等待线程。这个时候,等待线程就可以醒来,开开心心的获得结果啦。  

最后我们看一下任务取消  

3 public boolean cancel(boolean mayInterruptIfRunning)

注意,取消操作不一定会起作用,这里我们先贴个demo

 public class FutureDemo {
public static void main(String[] args) {
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
// 预创建线程
executorService.prestartCoreThread(); Future future = executorService.submit(new Callable<Object>() {
@Override
public Object call() {
System.out.println("start to run callable");
Long start = System.currentTimeMillis();
while (true) {
Long current = System.currentTimeMillis();
if ((current - start) > 1000) {
System.out.println("当前任务执行已经超过1s");
return 1;
}
}
}
}); System.out.println(future.cancel(false)); try {
Thread.currentThread().sleep(3000);
executorService.shutdown();
} catch (Exception e) {
//NO OP
}
}
}

我们多次测试后发现,出现了2种打印结果,如图

                结果1

                结果2    

第一种是任务压根没取消,第二种则是任务压根没提交成功。 

方法签名注释告诉我们,取消操作是可能会失败的,如果当前任务已经结束或者已经取消,则当前取消操作会失败。如果任务尚未开始,那么任务不会被执行。这就解释了出现上图结果2的情况。我们还是从源码去分析cancel()究竟做了哪些事。

public boolean cancel(boolean mayInterruptIfRunning) {
if (state != NEW)
return false;
if (mayInterruptIfRunning) {
if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, INTERRUPTING))
return false;
Thread t = runner;
if (t != null)
t.interrupt();
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); // final state
}
else if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED))
return false;
finishCompletion();
return true;
}

  执行逻辑如下

  1. state不为NEW时,任务即将进入终态,直接返回false表明取消操作失败。
  2. state状态为NEW,任务可能已经开始执行,也可能还未开始。
  3. mayInterruptIfRunning表明是否中断线程。若是,则尝试将state设置为INTERRUPTING,并且中断线程,之后将state设置为终态INTERRUPTED。
  4. 如果mayInterruptIfRunning=false,则不中断线程,把state设置为CANCELLED
  5. 移除等待线程并唤醒。
  6. 返回true

可见,cancel()方法改变了futureTask的状态位,如果传入的是false并且业务逻辑已经开始执行,当前任务是不会被终止的,而是会继续执行,直到异常或者执行完毕。如果传入的是true,会调用当前线程的interrupt()方法,把中断标志位设为true。

事实上,除非线程自己停止自己的任务,或者退出JVM,是没有其他方法完全终止一个线程的任务的。mayInterruptIfRunning=true,通过希望当前线程可以响应中断的方式来结束任务。当任务被取消后,会被封装为CancellationException抛出。

总结

  总结一下,futureTask中的任务状态由变量state表示,任务状态都基于state判断。而futureTask的阻塞则是通过自旋+挂起线程实现。理解FutureTask的内部实现机制,我们使用Future时才能更加得心应手。文中掺杂着笔者的个人理解,如果有不正之处,还望读者多多指正

作者:mayday芋头

本博客中未标明转载的文章归作者mayday芋头和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利

  

 

揭密FutureTask的更多相关文章

  1. Atitit s2018 s4 doc list dvchomepc dvccompc.docx .docx \s2018 s4 doc compc dtS44 \s2018 s4 doc dvcCompc dtS420 \s2018 s4f doc homepc \s2018 s4 doc compc dtS44\(5 封私信 _ 44 条消息)WebSocket 有没有可能取代 AJAX

    Atitit s2018 s4 doc list dvchomepc dvccompc.docx .docx \s2018 s4 doc compc dtS44 \s2018 s4 doc dvcCo ...

  2. Java--FutureTask原理与使用(FutureTask可以被Thread执行,可以被线程池submit方法执行,并且可以监控线程与获取返回值)

    package com; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; i ...

  3. FutureTask的使用

    package org.zln.thread.pool.ft;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.ut ...

  4. java并发编程学习:如何等待多个线程执行完成后再继续后续处理(synchronized、join、FutureTask、CyclicBarrier)

    多线程应用中,经常会遇到这种场景:后面的处理,依赖前面的N个线程的处理结果,必须等前面的线程执行完毕后,后面的代码才允许执行. 在我不知道CyclicBarrier之前,最容易想到的就是放置一个公用的 ...

  5. Java并发编程:Callable、Future和FutureTask

    作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本博客中未标明转载的文章归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置 ...

  6. java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@1f303192 rejected from java.util.concurrent.ThreadPoolExecutor@11f7cc04[Terminated, pool size = 0, active threads

    java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@1f303192 rejec ...

  7. Java多线程21:多线程下的其他组件之CyclicBarrier、Callable、Future和FutureTask

    CyclicBarrier 接着讲多线程下的其他组件,第一个要讲的就是CyclicBarrier.CyclicBarrier从字面理解是指循环屏障,它可以协同多个线程,让多个线程在这个屏障前等待,直到 ...

  8. Callable、Future、RunnableFuture、FutureTask的原理及应用

    1. Callable.Future.RunnableFuture.FutureTask的继承关系 在多线程编程中,我们一般通过一个实现了Runnable接口的对象来创建一个线程,这个线程在内部会执行 ...

  9. 线程池之 Callable、Future、FutureTask

    java线程中的异步和同步,并不是走路,一定要搞清楚.那么join方法嘛,就是异步变同步.线程阻塞,就再楼下一直等着它想要的状态出现喽.直接上代码,先来看Future获取线程执行结果的使用示例: pu ...

随机推荐

  1. VS2010/MFC编程入门之二十(常用控件:静态文本框)

    上一节鸡啄米讲了颜色对话框之后,关于对话框的使用和各种通用对话框的介绍就到此为止了.从本节开始鸡啄米将讲解各种常用控件的用法.常用控件主要包括:静态文本框.编辑框.单选按钮.复选框.分组框.列表框.组 ...

  2. LFD,非官方的Windows二进制文件的Python扩展包

    LFD,非官方的Windows二进制文件的Python扩展包 LFD,非官方版本.32和64位.Windows.二进制文件.科学开源.Python扩展包 克里斯托夫·戈尔克(by Christoph ...

  3. zabbix server监控报主机 Lack of free swap space

    zabbix server监控报主机 Lack of free swap space,因为交换空间不足引起.该主机内存为3G,正常交换空间大小为物理内存2倍左右. #查看已有内存及交换空间 free ...

  4. mongodb的分片

    分片是把大型数据集合进行分区成更小的可管理的片的过程. 副本集的每一个成员(仲裁节点除外)都一份数据的完整拷贝! 分片的目的:在节省硬件成本的基础上,提高系统的整体性能.但是却增加了管理和性能的开销. ...

  5. Linux内核分析第五周 扒开系统调用的三层皮(下) (20135304 刘世鹏)

    作者:刘世鹏20135304  <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.给MenuOS增加t ...

  6. HDU 1317 XYZZY(floyd+bellman_ford判环)

    http://acm.hdu.edu.cn/showproblem.php?pid=1317 题意: 给出一个有向图,每到达一个点,都会加上或减去一些能量,我们要做的就是判断从1出发是否能到达n.初始 ...

  7. UVa 10791 最小公倍数的最小和(唯一分解定理)

    https://vjudge.net/problem/UVA-10791 题意: 输入整数n,求至少两个正整数,使得它们的最小公倍数为n,且这些整数的和最小. 思路: 首先对n进行质因数分解,举个例子 ...

  8. mybatis报Invalid bound statement (not found) 分析

      解决问题的步骤,请参考: 1.mapper.xml要和对应的mapper接口在同一个包下,包名要一模一样. 2.Mapper接口中的方法在Mapper.xml中没有,然后执行Mapper接口的方法 ...

  9. 添加 LogCat 到Eclipse

    当你第一次在 Eclipse 中运行 Android 项目的时候,Eclipse 会提醒你一次是否要添加 LogCat 这个工具. 如果你现在还没有添加上的话,我这里教你一下如何手动添加 LogCat ...

  10. [原][osgearth]earth文件加载道路一初步看见模型道路

    时间是2017年2月5日17:16:32 由于OE2.9还没有发布,但是我又急于使用OE的道路. 所以,我先编译了正在github上调试中的OE2.9 github网址是:https://github ...