FutureTask原理解析
原文链接:http://www.studyshare.cn/blog-front/blog/details/1130
首先写一个简单的Demo
public static void main(String[] args) throws Exception{
FutureTask futureTask =new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
long startTime = System.currentTimeMillis();
int count =0;
//进行累加,让子线程执行一段时间
for(int i=0;i<2000000000;i++){
count += i;
}
System.out.println("count = " + count +" use time is "+ (System.currentTimeMillis()-startTime));
return "test";
}
});
new Thread(futureTask).start();
//get()在主线程中执行,阻塞方法,主线程等待子线程执行完才被唤醒执行获取子线程返回的结果
System.out.println(futureTask.get());
}
打印结果很简单:
首先解释FutureTask的成员变量含义,在它的各种方法中很多地方使用
volatile int state :用来表示当前执行线程的状态,用volatile关键字修饰,表示内存可见性,其他线程修改volatile修饰的state后,子线程会强 制到主内存取state最新的值,可参考文章:https://www.cnblogs.com/daxin/p/3364014.html
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;//已中断状态
private Callable callable : Callable接口变量,外部实例化传递到FutureTask中
private Object outcome :用于存放返回结果或者异常信息
private volatile Thread runner :存放当前子线程(执行任务的线程)
private volatile WaitNode waiters :一个节点内部类,类中包含
static final class WaitNode {
volatile Thread thread;//线程成员变量
volatile WaitNode next;//下一个包装了等待线程的节点
WaitNode() {thread = Thread.currentThread(); }//初始化时当前线程设置为需要等待的线程
}
当前子线程执行过程中以上状态会有以下几种路线:
1. NEW -> COMPLETING -> NORMAL : 新建 --> 执行--> 正常结束
2. NEW -> COMPLETING -> EXCEPTIONAL:新建--> 执行--> 异常结束(执行过程中发生异常)
3. NEW -> CANCELLED : 新建--> 取消(准备执行时主线程调用子线程的取消方法)
4. NEW -> INTERRUPTING -> INTERRUPTED :新建--> 被通知中断(准备执行时主线程调用子线程取消方法并传递中断标志位true)-->已中断
FutureTask的主要方法:
1.FutureTask(Callable callable):构造方法,初始化传入的callable,并将state设置为NEW
2.isCancelled() :获取当前执行中的线程状态是否已经取消,已取消返回true,否则返回false
3.isDone() : 获取当前执行的线程的状态是否处于运行状态,是返回true,否则返回false
4.cancel(boolean mayInterruptIfRunning):发出取消或者中断当前线程的信号。参数为true,则发出中断,否则为取消
5.get()、get(long timeout, TimeUnit unit) :获取线程执行完成后的返回结果
6.set(V v):设置线程的结果值到成员变量outcome 中
7.setException(Throwable t):设置线程出现异常时的异常值到成员变量outcome 中
8.run():FutureTask实现了Runnable接口,因此需要覆盖run()方法,call()方法就是在run()方法中进行调用
9.finishCompletion():该方法是执行的线程完成(无论正常完成还是异常完成)后唤醒其他等待的线程继续执行。并清空callable。
10.awaitDone(boolean timed, long nanos):核心方法,当执行线程在运行中,主线程调用get()方法后则加入等待队列,类似AQS的同步队列,执行的线程执行完毕会调用finishCompletion()方法唤醒等待队列中的线程进行执行。
下面对Demo中的代码进行源码分析:
第一步:执行构造方法
public FutureTask(Callable callable) {
if (callable ==null) //判断传入的callable不为空,则初始化FutureTask中的成员变量
throw new NullPointerException();
this.callable = callable;
this.state =NEW; // ensure visibility of callable
}
第二步:new Thread(futureTask).start();
此处实例化一个线程并调用start()方法,则操作系统调度该线程,并分配CPU时间片,如果该线程获取到CPU时间片后,则会执行run方法
注意:由于使用的是FutureTask,该类实现了Runnable接口并覆写了run方法,则会进入到FutrueTask的run方法中执行,代码如下:
public void run() {
//首先判断state是否是处于新建以外的状态(如果是新建以外的状态则直接返回,因为线程刚进入run只会是new状态,为了程序健壮性做此判断),compareAndSwapObject是判断当前FutureTask中的runner是否是null,为空则将当前执行的这个线程赋值给runner,不为空就直接退出
if (state !=NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
return;
try {
Callable c =callable;
if (c !=null &&state ==NEW) { //判断状态与callable
V result;
boolean ran;
try {
result = c.call(); //执行Callable中的call()方法
ran =true;
}catch (Throwable ex) {//如果执行call()方法出现了异常,则捕获异常并去设置异常信息
result =null;
ran =false;
setException(ex);//捕获异常并去设置异常信息,将异常信息设置到Object outcome结果对象中,同时改变state的状态为EXCEPTIONAL
}
if (ran)
set(result);//正常执行,设置返回结果,设置到Object outcome中,并设置state状态为NORMAL
}
}finally {
runner =null;//清空runner,方便GC回收
int s =state;
if (s >=INTERRUPTING) //如果state状态为中断中或者已中断
handlePossibleCancellationInterrupt(s);//交出当前线程的执行权,与其他线程重新竞争
}
}
此处看一下setException()与set()方法内的源码
setException():
protected void setException(Throwable t) {
//原子操作比较state的内存地址上的值是否与NEW相等,相等则将state修改为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;//使用outcome保存异常结果信息
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // 将state设置为EXCEPTIONAL
finishCompletion();//下面贴出源码,具体解释
}
}
set():
protected void set(V v) {
//原子操作比较state的内存地址上的值是否与NEW相等,相等则将state修改为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;//使用outcome保存正常返回的结果信息
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 将state设置为NORMAL
finishCompletion();
}
}
finishCompletion():这是线程执行(正常或者异常)的最后一个方法,下面分析一下源码
private void finishCompletion() {
// 无论执行的线程是正常还是异常都会返回结果并设置到outcome中,那么返回结果后,主线程或者其他线程还在等待队列中,则需要去唤醒等待队列中的线程进行执行。
for (WaitNode q; (q =waiters) !=null;) {//循环等待队列中的线程节点
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {//判断等待线程节点是否与之前设置的等待线程一致,一致则返回true
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; // 置空,方便GC回收
q = next;//当前等待线程节点也置空,在上面已经唤醒了当前的等待线程,因此此处也将已经唤醒的线程置空让GC回收
}
break;
}
}
done();
callable =null; // 当前线程中的callable置空,让gc回收,整个线程到此执行完毕
}
以上详细说明线程start()方法调用后的一系列执行过程。那么有个问题就是这个等待线程节点是从哪里产生的呢?下面解释这个问题
看Demo中的代码: System.out.println(futureTask.get()); 这句代码在demo有解释,get()方法是一个阻塞的方法,那么就是这个get()
方法产生的WaitNode节点的。看源码:
public V get() throws InterruptedException, ExecutionException {
int s =state;
if (s <=COMPLETING) //判断state是否处于执行中或者新建状态,是的话则说明调用get()方法的线程需要进入等待队列,等待callable所在的线程执行完毕后并等待唤醒,此处的代码就证明了get()方法是阻塞的
s = awaitDone(false, 0L);//下面分析源码
return report(s);
}
awaitDone():
private int awaitDone(boolean timed, long nanos) throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos :0L;//引入等待超时机制,调用get()方法传false,0L则表示不进行超时处理
WaitNode q =null;
boolean queued =false;
for (;;) {//自旋
if (Thread.interrupted()) {//判断当前线程(主线程)是否已经中断,中断则去移除等待线程节点并抛出中断异常,此代码为程序健壮性考虑,不必过分关注
removeWaiter(q);
throw new InterruptedException();
}
int s =state;
if (s >COMPLETING) {//如果执行的子线程的state状态已经处于NORMAL或者EXCEPTIONAL状态,说明子线程已经执行结束了,那么直接返回,说明不用让阻塞线程进入等待队列。
if (q !=null)//等待线程节点不为空,则直接置为空,并返回state
q.thread =null;
return s;
}
else if (s ==COMPLETING)// 重点,当当前执行子线程还在运行中的时候,则此时要让主线程交出CPU执行权。
Thread.yield();//交出CPU执行权,yield()方法虽然是交出执行权,但主线程还是可以和其他线程进行公平竞争
else if (q ==null)//重点,以上的条件都不满足,则说明该线程确实需要进入等待队列进行等待
q =new WaitNode();//构造一个等待节点,该构造方法中将当前线程作为参数传递给节点中的局部变量进行保存
else if (!queued)//注意,此处较难理解,这是入队列的操作,q.next=waiters ,如果是首节点,waiters一定是null的,则q.next=null,waitersOffset偏移量指向的地址上的初始值也是null,则期望值与内存地址值都为null,则会将q的值设置到waitersOffset指向的地址,同时返回true,等待节点进入队列就成功。原子操作比较抽象,建议去深入理解CAS操作
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);//自旋到这里的时候,说明已经加入等待队列,阻塞当前线程,让其等待被唤醒。
}
}
另外最后获取返回值的方法report()比较简单就不多做解释。内部就是返回outcome的值
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);
}
到此,FutureTask源码就分析完毕,总结一下:Callable实现的线程内部使用state的转换,这种转换是基于原子操作来保证线程安全(多线程环境下对state进行竞争),同时其他非Callable的外部线程调用FutureTask中的方法(主要是get()),则让这些线程进入等待队列,当Callable的线程执行完毕会使用自旋对等待中的线程进行唤醒。
更多深度技术好文:http://www.studyshare.cn/blog-front/index
FutureTask原理解析的更多相关文章
- java线程池原理解析
五一假期大雄看了一本<java并发编程艺术>,了解了线程池的基本工作流程,竟然发现线程池工作原理和互联网公司运作模式十分相似. 线程池处理流程 原理解析 互联网公司与线程池的关系 这里用一 ...
- Java并发包JUC核心原理解析
CS-LogN思维导图:记录CS基础 面试题 开源地址:https://github.com/FISHers6/CS-LogN JUC 分类 线程管理 线程池相关类 Executor.Executor ...
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- Web APi之过滤器执行过程原理解析【二】(十一)
前言 上一节我们详细讲解了过滤器的创建过程以及粗略的介绍了五种过滤器,用此五种过滤器对实现对执行Action方法各个时期的拦截非常重要.这一节我们简单将讲述在Action方法上.控制器上.全局上以及授 ...
- Web APi之过滤器创建过程原理解析【一】(十)
前言 Web API的简单流程就是从请求到执行到Action并最终作出响应,但是在这个过程有一把[筛子],那就是过滤器Filter,在从请求到Action这整个流程中使用Filter来进行相应的处理从 ...
- GeoHash原理解析
GeoHash 核心原理解析 引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段 ...
- alibaba-dexposed 原理解析
alibaba-dexposed 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49821413 原理参考地址: htt ...
- 支付宝Andfix 原理解析
支付宝Andfix 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49802429 原理参考地址: http://blo ...
随机推荐
- 第五章JavaScript
创建数组://1.字面量方式创建 (推荐大家使用这种方式创建数组 简单粗暴) var colors = ['red','color','yellow'];console.log(colors) //空 ...
- 解决ubuntu输入正确用户密码重新跳到无法登录
解决方法:我们需要将.Xauthority的拥有者改为登陆用户(或者干脆将.Xauthority删除,此法转自网上,本人未验证)开机后在登陆界面按下shift + ctrl + F1进入tty命令行终 ...
- CCNet: Criss-Cross Attention for Semantic Segmentation 里的Criss-Cross Attention计算方法
论文地址:https://arxiv.org/pdf/1811.11721v1.pdf code address: https://github.com/speedinghzl/CCNet 相关论文 ...
- java8_api_正则表达式
正则表达式 什么是正则表达式 使用String类中的replaceAll方法 使用Pattern类编译正则表达式 使用Matcher类匹配正则表达式 什么是正则表达式 ...
- 2018-2019-2 网络对抗技术 20165308 Exp4 恶意代码分析
2018-2019-2 网络对抗技术 20165308 Exp4 恶意代码分析 实验过程 一.系统运行监控 (1)使用如计划任务,每隔一分钟记录自己的电脑有哪些程序在联网,连接的外部IP是哪里.运行一 ...
- 我提出了一个 Lean Html 5 的 概念 和 标准
提出 Lean Html 5 是因为 Html 可以作为 一个 应用程序 开发 的 标准 和 平台, 应用程序 包括 Web 程序 , 本地程序 , 桌面程序 , 嵌入式程序 , 串口通信 等 . L ...
- pache tomcat慢速HTTP拒绝服务攻击安全问题解决办法
问题说明:HTTP协议的设计要求服务器在处理之前完全接收到请求.如果HTTP请求未完成,或者传输速率非常低,则服务器将保持其资源占用等待剩余的数据.如果服务器占用的资源太多,则会造成拒绝服务. 漏洞危 ...
- FileUpload上传
单文件上传: ASPX: <div> <!-- 文件上传 --> <asp:FileUpload ID="FileUpload1" runat=&qu ...
- 关于memset的错误使用
我们在使用memset进行初始化的时候,经常会使用这种方式,memset(a,0,sizeof(a)),这让我们误以为将其初始化其他值也可以,实际是错误的. void print_arr(unsign ...
- C# .NET XML 序列化为对象,反序列化
如果遇到: 根级别上的数据无效. 行 1,位置 1 .:即无法反序列化(反序列失败),得到对象为null ,把 xml 文本 Trim一下. xml=xml.Trim(); 序列化完毕你可以看 ...