FutureTask源码分析

一、Futre介绍

主要特点:

1、可以拿到子线程的返回值

2、如果主线程要拿子线程的返回值,主线程会阻塞住

创建线程的方式一般常用的是 Thread、Runnable 的方式,如果需要当前处理的任务有返回值的话,需要使用 Callable,Callable需要配合 Future使用。

Future是一个接口,一般会使用 FutureTask实现类去接收 Callable任务的返回结果。

FutureTask存在一些问题,同步非阻塞执行的任务,他不会主动通知你返回结果是什么?

二、FutureTask的使用

简单的使用方式

  • Callable 是你要执行的任务
  • FutureTask 是存放任务返回的结果 --> 同步阻塞式的
csharp 复制代码
public static void main(String[] args) throws Exception {
    futrueTask();
}

// 测试FutureTask的使用
public static void futrueTask() throws Exception {
    // 里面的lambda表达式 是 Callabe的实现
    FutureTask<Integer> futureTask = new FutureTask<>(() -> {
        System.out.println("我是FutureTask,我先执行!!");
        Thread.sleep(2000);
        return 110;
    });

    Thread thread = new Thread(futureTask);
    System.out.println("我是主线程!!!");
    thread.start();
    System.out.println("阻塞住,等子任务跑完,才会拿到返回结果 : " + futureTask.get());
}
csharp 复制代码
public static void callableTask() throws Exception {
    // 测试使用 Collable
    Callable<Integer> callable = () -> {
        Thread.sleep(2000);
        System.out.println("我是 Callable,阻塞了 2秒");
        return 121;
    };
    FutureTask<Integer> futureTask = new FutureTask(callable);
    Thread thread = new Thread(futureTask);
    System.out.println("测试 Callable!!!!");
    thread.start();
    System.out.println("callable阻塞后的结果是:" + futureTask.get());
}

这块声明一点:

1、为什么 new Thread的时候,参数可以传FutureTask类型?

FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口,所以FutureTask可以说是 Runnable接口的实现类

2、查看 FutureTask的构造器

参数可以传 Callable的实现类,也可以传 Runnable的实现类 + 结果 --> Runnable的实现类不需要指定返回结果,所以需要指定返回结果。

三、FutureTask源码分析

FutureTask的功能只分析如下几个方法:

1、构造器相关方法

2、run() --> 线程启动时,调用的方法

3、get() --> 主线程阻塞获取返回结果

余下的方法,根据这几个方法往下延伸分析。

1、属性分析

arduino 复制代码
    /**
     *
     * 状态流转过程
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    private volatile int state; // 表示子线程的运行状态,包含如下七个状态
    
    private static final int NEW          = 0; // 初始状态,构造器new的时候初始化为0

    private static final int COMPLETING   = 1; // 任务执行完,生成返回结果,封装给 FutureTask

    private static final int NORMAL       = 2; // 整个子线程运行结束,FutureTask封装结束

    private static final int EXCEPTIONAL  = 3; // 执行任务时,发生异常

    private static final int CANCELLED    = 4; // 任务被取消

    private static final int INTERRUPTING = 5; // 线程的中断状态被设置为true(现在还在运行)

    private static final int INTERRUPTED  = 6; // 线程状态为中断

    /** 任务封装为 Callable,具体还得交给 Callable来运行 */
    private Callable<V> callable;
    /** 返回结果,FutureTask.get() 拿到的返回结果 */
    private Object outcome; 
    /** 具体执行的线程 */
    private volatile Thread runner;
    /** 如果有其他线程调用get()方法,需要挂起等待,这个单项链表用来存放需要挂起等待的线程 */
    private volatile WaitNode waiters;

2、构造器分析

php 复制代码
// 这里简单分析参数传递 Callable 的实现类的情况
public FutureTask(Callable<V> callable) {
    if (callable == null){
         throw new NullPointerException();   
    }
    this.callable = callable; // 赋值给成员变量参数 Callable
    this.state = NEW;       // 初始化 FutureTask的状态为 New
}

3、run()方法

Thread.start() --> 启动线程之后,会调用到run()方法 --> Callable的 call方法,是通过run方法来执行的call方法

csharp 复制代码
 public void run() {
        // 检查任务状态是否已经不是NEW(初始状态)
        // 或者 CAS操作:尝试将runner字段从null设置为当前线程 ,如果操作失败,return
        if (state != NEW || !RUNNER.compareAndSet(this, null, Thread.currentThread())){
            return;
        }
        // 准备执行任务
        try {
            Callable<V> c = callable;
            // 任务不为 null  并且 状态为初始化状态
            if (c != null && state == NEW) {
                // 放返回结果
                V result;
                // 任务执行是否为正常结束
                boolean ran;
                try {
                    // 运行call方法,拿到返回结果封装到result中
                    result = c.call();
                    // 任务正常结束
                    ran = true;
                } catch (Throwable ex) { // 出现异常
                    result = null; // 结果为 null
                    ran = false; // 异常返回,ran设置为false
                    setException(ex); // 设置异常信息
                }
                if (ran){
                    // 正常返回结束,封装返回结果
                    set(result);
                } 
            }
        } finally {
            // 将具体执行的线程置为 null
            runner = null;
            // 拿到状态
            int s = state;
            // 中断做一些后续处理
            if (s >= INTERRUPTING){
                handlePossibleCancellationInterrupt(s);
            }
        }
    }

中断处理流程

csharp 复制代码
// 中断处理流程
private void handlePossibleCancellationInterrupt(int s) {
    // 
    if (s == INTERRUPTING)
        // 终态是INTERRUPTED ,这里判断如果是中断设置为 true,则让出 CPU 的时间片
        while (state == INTERRUPTING){
             Thread.yield(); // wait out pending interrupt
        }
    // assert state == INTERRUPTED;
}

这里用自旋等待,不需要sleep的原因:

  • 从 INTERRUPTING 到 INTERRUPTED 速度极快,几纳秒即可,用yield让出 CPU 时间片,不需要sleep的上下文线程切换的开销
vbnet 复制代码
时间线:
t0: 状态=NEW, 任务运行中
t1: 线程A调用cancel(true)
    状态变为INTERRUPTING
    向运行线程发送中断信号
t2: 线程B调用get(),看到状态是INTERRUPTING
    进入handlePossibleCancellationInterrupt
    开始while循环
t3: 线程A完成中断,状态变为INTERRUPTED
t4: 线程B退出循环,继续执行

set方法

kotlin 复制代码
protected void set(V v) {
    // 通过 CAS 设置线程状态为completing
    if (STATE.compareAndSet(this, NEW, COMPLETING)) {
        // 结果赋值给 outcome
        outcome = v;
        // 设置状态为终态 NORMAL
        STATE.setRelease(this, NORMAL); // final state
        // 最终处理 --> 唤醒waiters单向链表里的阻塞线程
        finishCompletion();
    }
}

设置状态为 Normal终态 --> 赋值结果给成员属性 outcome --> 唤醒waiters里的阻塞线程

4、finishCompletion()方法

任务状态已经变为了NORMAL,做一些后续处理

java 复制代码
// 从waiters单向链表里取值依次唤醒线程
private void finishCompletion() {
    // assert state > COMPLETING;
    // 循环遍历取waiters里的节点
    for (WaitNode q; (q = waiters) != null;) {
        if (WAITERS.weakCompareAndSet(this, q, null)) { // 将成员属性waiters里的值置为null
            for (;;) { // 死循环从链表里取值
                // 取出节点里的线程值
                Thread t = q.thread;
                if (t != null) {
                    // 移除掉线程值
                    q.thread = null;
                    // 唤醒线程
                    LockSupport.unpark(t);
                }
                // 往后找,接着唤醒,记录next节点
                WaitNode next = q.next;
                if (next == null){ // 如果没有next节点,直接break,说明队列里没有阻塞的线程了
                    break;
                }
                // 删除q节点
                q.next = null; // unlink to help gc
                // q来到next
                q = next;
            }
            break;
        }
    }
    // 拓展方法,没有任何实现,可自己实现
    done();
    // 成员属性 Callable 设置为null,任务处理完成了,可以拜拜了
    callable = null;        // to reduce footprint
}

核心流程:唤醒阻塞线程 + 成员属性 callable置为null

5、get()方法

其他线程调用get()方法,阻塞来获取 FutureTask的结果

get()方法有两个:一个带时间,一个不带时间

最终都会调用到awaitDone()这个方法,我们就从简单的get方法来进行分析

scss 复制代码
public V get() throws InterruptedException, ExecutionException {
    // 拿到当前 FutureTask的状态
    int s = state;
    // 满足这个状态,说明可能还没有返回结果
    if (s <= COMPLETING){
        // 没到completing,进行阻塞,挂起线程,等待拿结果
        s = awaitDone(false, 0L);
    }
    // 拿到返回结果,想要拿到正常的返回结果,传进去的s参数得为 normal
    return report(s);
}

6、awaitDone()方法

这个方法,主要是其他线程调用,不是当前 FutureTask调用,不要搞混了

java 复制代码
/** 挂起线程,返回结果是int类型
* 线程等待任务执行结束,等待任务执行的状态变为大于 Completing状态
* timed参数:boolean值,是否传递时间参数
* nanos: 时间戳
*/
private int awaitDone(boolean timed, long nanos)throws InterruptedException {
    // 开始时间,用于计算超时
    long startTime = 0L;
    // 等待节点
    WaitNode q = null;
    // 节点是否已入队列
    boolean queued = false;
    // 自旋循环
    for (;;) {
        int s = state;
        if (s > COMPLETING) { // FutureTask的状态大于CANCELLED说明任务已经执行结束了
            if (q != null){ // 如果设置过waitNode,将thread值置为 null
                q.thread = null;
            }
            return s; // 返回状态值
        }else if (s == COMPLETING){ // 如果任务的状态处于completing
            // 让出CPU时间片 , completing的持续时间非常短,只需要让出cpu时间片即可,不需要睡眠
            Thread.yield();
        }else if (Thread.interrupted()) { // 判断当前线程是否中断
            // 已经中断,将当前节点从waiters中移除
            removeWaiter(q);
            // 抛出中断异常
            throw new InterruptedException();
        }else if (q == null) { // 节点为 null
            if (timed && nanos <= 0L){
                 // 设置了时间 并且 等待时间也到期了,直接返回
                 return s;
            }
            // 时间没到,new一个waitNode节点出来
            q = new WaitNode();
        }else if (!queued){ // 节点没入队列
            // 通过 CAS ,用头插法 链接上waiters,并且将waiters的值设置为 q
            queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
        }else if (timed) { // 根据时间来获取结果
            final long parkNanos;
            if (startTime == 0L) { // 头一次进来,设置startTime,取系统时间
                startTime = System.nanoTime();
                if (startTime == 0L){
                    startTime = 1L;
                }
                parkNanos = nanos;
            } else {
                // 第二次进来,计算跟头一次进来的差值
                long elapsed = System.nanoTime() - startTime;
                if (elapsed >= nanos) { // 判断差值是不是大于等于传参进来的值
                    removeWaiter(q); // 移除掉阻塞节点
                    return state; // 返回状态结果
                }
                // 第二次进来还没到等待的时间,更新等待的时间结果
                parkNanos = nanos - elapsed;
            }
            if (state < COMPLETING){ // 第二次进来,时间没到,并且FutureTask的任务还没到 Completing状态
                // 指定时间挂起线程
                LockSupport.parkNanos(this, parkNanos);
            }
        }else {
            // 挂起线程
            LockSupport.park(this);
        }
    }
}

// 内部类节点
static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    // 初始化thread为当前线程
    WaitNode() { thread = Thread.currentThread(); }
}

这里只是涉及到挂起,等run()方法执行完毕,会唤醒waiters里的所有节点

7、report()方法

java 复制代码
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL){ // 判断是否是normal状态
        // 说明正常结束,返回结果
        return (V)x;
    }
    // 非正常结束 cancelled / 中断 报异常
    if (s >= CANCELLED){
        throw new CancellationException();
    }
    // 异常结束
    throw new ExecutionException((Throwable)x);
}

四、总结

1、当前 FutureTask方法具体会执行run()方法 --> 封装了 Callable --> 具体执行的是callable的call方法 --> 唤醒其他阻塞线程

2、其他线程调用get()方法 --> 会扔到watiers单向链表等待队列里,两者配合

相关推荐
梨子同志4 小时前
Java 基础语法详解
后端
bcbnb4 小时前
详细教程:iOS应用中Swift代码混淆步骤与工具推荐
后端
expect7g4 小时前
Paimon源码解读 -- Compaction-8.专用压缩任务
大数据·后端·flink
开心就好20254 小时前
H5 混合应用加密 Web 资源暴露到 IPA 层防护的完整技术方案
后端
bcbnb4 小时前
最新版本iOS系统设备管理功能全面指南
后端
开心就好20254 小时前
Fastlane + Appuploader 的工程组合,自动化发布中的分工
后端
YDS8294 小时前
SpringCould —— 网关详解
后端·spring·spring cloud
华仔啊4 小时前
如何避免MySQL死锁?资深DBA的9条黄金法则
后端·mysql
老华带你飞4 小时前
列车售票|基于springboot 列车售票系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习·spring