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. uwsgi手动安装时报错ValueError: invalid literal for int() with base 10: '32_1'

    安装uwsgi,安装步骤如下 wget https://projects.unbit.it/downloads/uwsgi-latest.tar.gz tar zxvf uwsgi-latest.ta ...

  2. 【运维技术】VM虚拟机上使用centos7安装docker启动gogs服务教程【含B站视频教程】

    VM虚拟机上使用centos7安装docker启动gogs服务视频教程 BiliBili视频教程链接飞机票,点我 使用VMware Workstation安装Centos7 MinMal系统 第一步: ...

  3. Python3:Requests模块的异常值处理

    Python3:Requests模块的异常值处理 用Python的requests模块进行爬虫时,一个简单高效的模块就是requests模块,利用get()或者post()函数,发送请求. 但是在真正 ...

  4. linux内核与分析 心得与体会

    作业目录: (1)计算机是如何工作的:http://www.cnblogs.com/20135335hs/p/5213394.html (2)操作系统是如何工作的:http://www.cnblogs ...

  5. android的hook方面知识点

    android hook分为另种: native层hook---理解ELF文件 java层---虚拟机特性和Java上的反射的作用 注入代码: 存放在哪? 用mmap函数分配临时内存来完成代码存放,对 ...

  6. double、float等多字节数据处理

    一.常规的多字节: 有2,4,8字节 float和double是具有自身算法的数据类型,和其他整型不一样[整型数据,可以直接通过移位来进行计算值的大小,float和double不行] 值 = 尾数x ...

  7. LS1021ATWR开发板启动日志分析

    一.背景 LS1021ATWR开发板运行官方的openwrt系统 二.日志分析 2.1 linux相关日志 root@OpenWrt:/# reboot  重启 root@OpenWrt:/# [ 2 ...

  8. HDU 3709 Balanced Number(数位DP)题解

    思路: 之前想直接开左右两边的数结果爆内存... 枚举每次pivot的位置,然后数位DP,如果sum<0返回0,因为已经小于零说明已经到了pivot右边,继续dfs只会越来越小,且dp数组会炸 ...

  9. HDU 3065 病毒侵袭持续中(AC自动机)题解

    题意:要你找到主串中每个模式串的个数. 思路:题目都没说是多组数据,结果没while(~)直接WA了,和上一题差不多,可以用map或者开个数组储存.指针要记得回收内存,不然MLE. #include& ...

  10. 解决 docker: Error response from daemon: ... : net/http: TLS handshake timeout.

    参考:解决 Docker pull 出现的net/http: TLS handshake timeout 的一个办法 问题: 执行 $ sudo docker run hello-world 时出现: ...