基于源码分析Java FutureTask任务执行原理

前言

        之前有分享关于FutureTask异步编程获取线程执行结果的文章,获取异步线程执行结果多种姿势,当时就在思考FutureTask的相关实现原理,应该不会太复杂,这两天抽空看了一下源码,觉得简单清晰且有助于对Java JUC下多线程思想的理解,就把自己对于这块的理解记录分享出来。本文内容主要包括针对FutureTask基本属性,关键方法的解析。

正文

FutureTask使用

        对于FutureTask的使用,一个非线程池的demo及FutureTask对应的类图如下:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        Integer result = futureTask.get();//获取任务执行结果
        result = FutureTask.get(10, TimeUnit.SECONDS);//获取任务执行结果,阻塞超时时间10s
        FutureTask.cancel(true);//取消执行任务
        System.out.println(futureTask.isDone());
        System.out.println(futureTask.isCancelled());
    }

future类图.png
        从类图中可以看到FutureTask实现了runnable接口,因此可以作为Thread的构造函数参数传入,并执行任务,执行的内容为FutureTask构造函数中的callable或者runnable接口中对应的实现方法,最后可以通过get方法获取执行结果。

        我们在深入源码之前,可以先思考一下,如果是自己去设计FutureTask这块的功能实现,应该如何考虑,带着问题学习源码会更高效。

  1. 首先是实现了runnable接口,代表FutureTask实现了run方法,其中子线程(Thread.start())可以执行run方法中的逻辑;
  2. 其次可以通过get方法在主线程阻塞性的获取结果,支持超时时间设定;
  3. 可以取消任务、查看任务当前状态。

        由上我们大概可以猜测以下几点主要的实现:

  1. FutureTask重写的run方法中有对自身构造函数callable方法的调用,利用操作系统底层能力创建线程执行任务;
  2. 其次FutureTask内部会维护任务执行的各种状态,状态的切换需要保证线程安全;
  3. 最后是普通get方法和重载带超时时间参数的get方法实现,任务未完成时,主线程的get函数会一直阻塞请求结果,当任务执行完毕时修改对应任务状态,将返回数据放到一个Object对象中,然后主get方法中检查到状态变更,就直接获取返回对应的object对象。

FutureTask基本属性

        接着我们验证自己的猜想与FutureTask中的真正的实现,先看FutureTask的基本属性,如下:

//当前任务执行状态
private volatile int state;

//构造函数传入的任务本身对象
private Callable<V> callable;

//object类型的任务执行结果
private Object outcome;

//执行任务的线程
private volatile Thread runner;

//调用get方法被阻塞的主线程是waitnode类型
private volatile WaitNode waiters;

        所以基本上与猜想一致,FutureTask为了实现任务调度、查看任务状态、获取执行结果等功能,内部字段有任务状态、任务本身、执行结果、线程等。
        而为什么需要多出来WaitNode,这就是没考虑到的点了,主线程调用get方法会阻塞获取请求结果,按照我的设计是一直hang着主线程轮询获取结果即可,但没有想到这样做是对cpu资源的严重浪费,而WaitNode的出现就是为了将等待线程封装起来后进行唤醒的数据结构,后面会详细说。
        下面来看看这些关键属性:

任务状态基本属性——state

        state字段以volatile关键字修饰,保证多线程查询时的可见性,从注释中可以看出,每个任务都是从NEW状态开始,可能会以NORMAL(正常结束)、EXCEPTIONAL(抛出异常)、CANCELLED(被取消)、INTERRUPTED(被中断)几个状态结束。

        而COMPLETING的状态虽然字面意思上是任务已完成,但是还需要填充任务执行结果字段outcome,因此可以理解为从COMPLETING到NORMAL或者EXCEPTIONAL的状态变更是非常短暂的。

    /*
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    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;

主线程阻塞get的等待封装对象WaitNode

        WaitNode如下所示以内部类的形式存在于FutureTask中,主要是为了优化get方法阻塞主线程的问题,如果没有WaitNode,那么主线程将会一直轮询重试,如果子任务执行时间较长或者获取get结果的主线程数量过多,肯定会大量占用cpu资源,因此引入WaitNode作为所有等待结果的线程链表或队列。
       一旦有线程执行get方法获取结果,并且任务并未完成,就需要新建WaitNode节点添加到等待队列中;后续的任务完成,会依次通知等待队列中的所有线程。WaitNode的数据结构也很简单,一个是当前线程对象,一个是等待链表的next指针。

static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

FutureTask的关键方法逻辑

        FutureTask方法也相对精简,其中执行任务的run方法和获取任务执行结果的get方法逻辑比较核心,着重来看一下。

构造函数

        构造函数来看,FutureTask除了支持callable对象,还支持不带返回值的runnable对象,通过传入泛型参数来定义任务执行结果,runnable会被转换为callable对象。

    //callable对象的构造函数
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    //同样支持runnable对象的构造函数,通过工具类转换为callable对象以便后续统一处理
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

run方法

        FutureTask实现了Runnable接口,重写了对应的run方法,因此在Thread执行start时,会启用操作系统能力另起一个线程调用其中的run方法,因此run方法是FutureTask执行任务的主要逻辑。代码如下:

    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);
        }
    }

futuretask run方法.png
        源码相对比较简洁,其中有很多利用操作系统底层的cas能力,对任务状态、waitnode进行变更操作。基本流程就是切换工作线程runner,开始执行任务,更改任务状态,放入执行结果,在任务执行完后利用waitnode链表唤醒通知等待线程,最后如果有中断则等待中断状态完成。

get方法

        主线程get获取任务执行结果接口,判断了一下任务状态,后续根据任务状态选择是否进入awaitDone方法等待结束。

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

        get方法主要逻辑封装在private int awaitDone(boolean timed, long nanos),awaitDone的timed参数即为是否设置超时的get方法。整个方法中不断循环判断当前任务的不同状态,从而对当前获取结果即执行get方法的主线程做相应操作。

  1. 已完成:直接返回对应数据;
  2. 完成中:thread.yield()自旋等待状态变为完成;
  3. 设置了超时时间且未完成,当前线程挂起对应的时间,后续唤醒后继续执行循环;
  4. 其他场景:直接挂起该线程,加入waitnode,等待任务执行完毕后通过waitnode链表依次唤醒主线程,继续死循环,走到1和2的场景中。
    future task get方法.png

cancel方法

        cancel方法相对来说就简单多了,内部逻辑主要是对任务状态进行变更,中断当前执行任务的runner线程,最后执行类似于上面run方法中的“执行后处理”的逻辑,完成对于任务线程的中断。

WaitNode移除等待节点方法

        WaitNode是针对get方法阻塞的线程构建的等待队列,在一些等待超时的场景中需要将当前线程移除调用removeWaiter。

private void removeWaiter(WaitNode node) {
        if (node != null) {
            node.thread = null;
            retry:
            for (;;) {          // restart on removeWaiter race
                for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                    s = q.next;
                    if (q.thread != null)
                        pred = q;
                    else if (pred != null) {
                        pred.next = s;
                        if (pred.thread == null) // check for race
                            continue retry;
                    }
                    else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                          q, s))
                        continue retry;
                }
                break;
            }
        }
    }

        一个多线程环境下,移除链表中某个节点的算法,算法中以node.thread == null 做了前置标记,后续迭代到队列中thread为null的node,即为需要移除的node。当定位node为需要移除的node时,分为了一下两种场景:,这里让我感觉到疑惑的点是,为了防止并发问题,为何在非头结点时不使用cas呢?
1.需要移除的node为头结点,如果为头结点,直接使用cas将第二个node对象赋值给waiters。
2.需要移除的node非头结点,使用前缀pre节点指向后续节点,简单的移除操作。
waitnode移除图.png
        之所以两个情况在操作移除node后,依旧需要continue retry的逻辑,是因为removeWaiter并不是一个线程安全的方法,在多线程的操作情况下,对于node的初始化标记的即thread=null的操作可能有并发,因此需要继续迭代一遍队列后才break。

总结

        整个FutureTask的源码分析就讲完了,相对其他工具类的逻辑与属性都比较简单,其中也穿插了不少细节,比如volatile修饰的各种属性保证多线程可见,volatile+cas的线程安全操作方式,构造函数的兼容性,考虑到多个主线程get时阻塞等待时对于cpu资源浪费引入的WaitNode,队列移除等待线程节点应对并发的两种场景考虑,以上都是值得借鉴的多线程编程思想。

        其中WaitNode这种思想在juc的其他工具类中使用也非常常见,比如AQS的同步队列,sychronized锁的cxq队列,waitSet等数据结构,都是为了线程获取资源时减少不必要的等待消耗计算资源,使用先进入等待队列后再资源准备就绪后唤醒(unpark)的策略,思想都是互通的。

参考

美团技术动态线程池

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值