并发编程—— FutureTask 源码分析
1. 前言
当我们在 Java 中使用异步编程的时候,大部分时候,我们都会使用 Future,并且使用线程池的 submit 方法提交一个 Callable 对象。然后调用 Future 的 get 方法等待返回值。而 FutureTask 是 Future 的一个实现,也是我们今天的主角。
我们就从源码层面分析 FutureTask.
2. FutureTask 初体验
我们一般接触的都是 Future ,而不是 FutureTask , Future 是一个接口, FutureTask 是一个标准的实现。在我们向线程池提交任务的时候,线程池会创建一个 FutureTask 返回。
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
newTaskFor 方法就是创建一个了一个 FutureTask 返回。
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
而线程池就会执行 FutureTask 的 run 方法。
那么,我们看看 FutureTask 的 UML。
可以看出,FutureTask 实现了 Runnable,Future 。Runnable 就不必说了,一个 run 方法,那 Future 呢?
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
主要是这 5 个方法撑起了 Future,功能相对而言比较薄弱,毕竟这只是一个 Future ,而不是 Promise。
FutureTask 还有一个内部类,WaitNode ,结构如下:
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
看起来是不是和 AQS 的节点似曾相识呢?
FutureTask 内部维护了一个栈结构,和 AQS 的队列有所区别。
实际上,在之前的版本中,FutureTask 确实直接使用的 AQS ,但是 Doug lea 又对该类进行了优化,优化的目的是 :
主要是为了避免有些用户在取消竞争期间保留中断状态。
而内部依然使用了一个 volatile 的 state 变量来控制状态,同时使用了一个栈结构来保存等待的线程。
至于原因,当然是 FutureTask 的 get 方法是支持并发的,多个线程可以获取到同一个 FutureTask 的同一个结果,而这些线程在 get 的阻塞过程中必然是要挂起自己等待的。
知道了 FutureTask 的结构。我们知道,线程池肯定会执行 FutureTask 的 run 方法,所以,我们到他的 run 方法看看。
同时,我们也要看看关键方法 —— get 方法。
3. FutureTask 的 get 方法
代码如下:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
首先判断状态,然后挂起自己等待,最后,返回结果,代码很简单。
注意:FutureTask 中有 7 种状态:
private volatile int state;
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;
构造时,状态就是 NEW,当任务完成中,状态变成 COMPLETING。当任务彻底完成,状态变成 NORMAL。
我们重点看看 awaitDone 和 report 方法。
awaitDone 方法代码:
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) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
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
LockSupport.park(this);
}
}
上面的方法相对于 JUC 其他的类,还是比较简单的。需要注意一个点:get 方法是可以并发访问的,当并发访问的时候,需要将这些线程保存在 FutureTask 内部的栈中。
简单说说方法步骤:
- 如果线程中断了,删除节点,并抛出异常。
- 如果字体大于 COMPLETING ,说明任务完成了,返回结果。
- 如果等于 COMPLETING,说明任务快要完成了,自旋一会。
- 如果 q 是 null,说明这是第一次进入,创建一个新的节点。保存当前线程引用。
- 如果还没有修改过 waiters 变量,就使用 CAS 修改当前 waiters 为当前节点,这里是一个栈的结构。
- 根据时间策略挂起当前线程。
- 当线程醒来后,继续上面的判断,正常情况下,返回数据。
再看看 report 方法:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
也还是很简单的,拿到结果,判断状态,如果状态正常,就返回值,如果不正常,就抛出异常。
总结一下 get 方法:
FutureTask 通过挂起自己等待异步线程唤醒,然后拿去异步线程设置好的数据。
4. FutureTask 的 run 方法
上面总结说,FutureTask 通过挂起自己等待异步线程唤醒,然后拿去异步线程设置好的数据。
那么这个过程在哪里呢?答案就是在 run 方法里。我们知道,线程池在执行 FutureTask 的时候,肯定会执行他的 run 方法。所以,我们看看他的 run 方法:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
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 = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
方法逻辑如下:
- 判断状态。
- 执行 callable 的 call 方法。
- 设置结果并唤醒等待的所有线程。
看看 set 方法是如何设置结果的:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
先将状态变成 COMPLETING,然后设置结果,再然后设置状态为 NORMAL,最后执行 finishCompletion 方法唤醒等待线程。
finishCompletion 代码如下:
private void finishCompletion() {
// assert state > COMPLETING;
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;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
该方法先将 waiters 修改成 null,然后遍历栈中所有节点,也就是所有等待的线程,依次唤醒他们。
最后执行 done 方法。这个方法是留个子类扩展的。FutureTask 中是个空方法。比如 Spring 的 ListenableFutureTask 就扩展了该方法。还有 JUC 里的 QueueingFuture 类也扩展了该方法。
如果异常了就将状态改为 EXCEPTIONAL。
如果用户执行了 cancel(true)方法。该方法 Java doc 如下:
试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。
也就是说,这个 mayInterruptIfRunning 决定当任务已经在执行了,还要终止这个任务。如果 mayInterruptIfRunning 是 true ,就会先将状态改成 INTERRUPTING,然后调用线程的 interrupt 方法,最后,设置状态为 INTERRUPTED。
在 run 方法的 finally 块中,对 INTERRUPTING 有判断,也就是说,在 INTERRUPTING 和 INTERRUPTED 的这段时间,会执行 finally 块,那么这个时候,就需要自旋等待状态变成 INTERRUPTED。
具体代码如下:
private void handlePossibleCancellationInterrupt(int s) {
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
}
5. 总结
关于 FutureTask 就介绍完了,该类最重要的就是 get 方法和 run 方法,run 方法负责执行 callable 的 call 方法并设置返回值到一个变量中, get 方法负责阻塞直到 run 方法执行完毕任务唤醒他,然后 get 方法回去结果。
同时,FutureTask 为了多线程可以并发调用 get 方法,使用了一个栈结构保存所有等待的线程。也就是说,所有的线程都等得到 get 方法的结果。
虽然 FutureTask 的设计很好,但我仍然觉得使用异步是更好的选择,效率更高。
并发编程—— FutureTask 源码分析的更多相关文章
- Java并发编程-ReentrantLock源码分析
一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...
- Java并发编程 ReentrantLock 源码分析
ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...
- 并发编程 —— Timer 源码分析
前言 在平时的开发中,肯定需要使用定时任务,而 Java 1.3 版本提供了一个 java.util.Timer 定时任务类.今天一起来看看这个类. 1.API 介绍 Timer 相关的有 3 个类: ...
- Java并发编程-AbstractQueuedSynchronizer源码分析
简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...
- Java并发编程 LockSupport源码分析
这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. package java.util.concurrent.locks ...
- java 并发编程——Thread 源码重新学习
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- FutureTask 源码分析
FutureTask 源码分析,这个类的原理与我分析android当中的FutureTask类差不多[http://www.cnblogs.com/daxin/p/3802392.html] publ ...
- 并发工具CyclicBarrier源码分析及应用
本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: 并发工具CyclicBarrier源码分析及应用 一.CyclicBarrier简介 1.简介 CyclicBarri ...
- Java异步编程——深入源码分析FutureTask
Java的异步编程是一项非常常用的多线程技术. 之前通过源码详细分析了ThreadPoolExecutor<你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识&g ...
随机推荐
- ubuntu 修改mysql 5.7数据库密码
1.vi /ect/mysql/debian 查看debain-sys-maint用户的密码 2.登录mysql 4.切换到mysql数据库,更新 user 表: update user set au ...
- [javascript-debug-ajax-json]两种不同的json格式数据
Bug 1: 1. 这里面的 data 只是一维数组{"state":0,"errorCode":0,"data":{"origi ...
- 应该知道的Linux技巧【转】
这篇文章来源于Quroa的一个问答<What are some time-saving tips that every Linux user should know?>—— Linux用户 ...
- 两台linux之间建立信任关系,实现免密码ssh远程登录或scp数据上传
两台linux之间建立信任关系,实现免密码远程登录或数据上传 1.执行ssh-keygen命令,生成建立安全信任关系的证书: linux1上:执行命令 ssh-keygen -t rsa 在程序提 ...
- WPF Adorner
之前做项目时,为了实现类似微信消息数目的效果 image.png ,我之前是修改的ControlTemplate.类似于将一个带数字的控件,放在另一个控件的右上角,来实现的这个效果. 原来WPF有 ...
- Android 四大组件之“ BroadcastReceiver ”
前言 Android四大组件重要性已经不言而喻了,今天谈谈的是Android中的广播机制.在我们上学的时候,每个班级的教室里都会装有一个喇叭,这些喇叭都是接入到学校的广播室的,一旦有什么重要的通知,就 ...
- dapper视频
dapper是dotnet下的一种小巧快捷的ORM框架,本视频主要讲解了dapper的多库使用,以及常见的操作,如:对象查询.多集合查询,关联查询等,添加.修改.删除等. 视频地址:https://w ...
- VMware中安装Contos
1 检查BIOS虚拟化支持 2 新建虚拟机 3 新建虚拟机向导 4 创建虚拟空白光盘 5 安装Linux系统对应的CentOS版 6 虚拟机命名和定位磁盘位置 7 处理器配置,看自己是否是双核.多核 ...
- C++获取本机IP地址信息
#include<winsock2.h> #include<iostream> #include<string> using namespace std; #pra ...
- C# 开发代码标准
开发标准文件 文件名称:C#开发规范 版 本:V2.0 前言 目的是为了规范每个人的编程风格,为确保系统源程序可读性,从而增强系统可维护性,制定下述编程规范,以规范系统各部分编程.系统继承的其它资源中 ...