美文网首页Java开发那些事
谈谈FutureTask的实现

谈谈FutureTask的实现

作者: 宸小朔 | 来源:发表于2019-06-10 22:26 被阅读17次

    概述

    ​ 在使用java多线程解决问题的时候,为了提高效率,我们常常会异步处理一些计算任务并在最后异步的获取计算结果,这个过程的实现离不开Future接口及其实现类FutureTask。FutureTask类实现了Runnable, Future接口,接下来我会通过源码对该类的实现进行详解。

    使用

    ​ 我们先看下FutureTask中的主要方法如下,可以看出FutureTask实现了任务及异步结果的集合功能。看到这块的方法,大家肯定会有疑问,Runnable任务的run方法返回空,FutureTask如何依靠该方法获取线程异步执行结果,这个问题,我们在下面给大家介绍。

    //以下五个方法实现接口Future中方法
    public boolean isCancelled();           
    public boolean isDone();       
    public boolean cancel();
    public V get() throws InterruptedException, ExecutionException;
    public V get(long timeout, TimeUnit unit);
    //实现接口Runnable中方法
    public void run();
    

    我们在使用中会构造一个FutureTask对象,然后将FutureTask扔到另一个线程中执行,而主线程继续执行其他业务逻辑,一段时间后主线程调用FutureTask的get方法获取执行结果。下面我们看一个简单的例子:

    /**
     * Created by yuanqiongqiong on 2019/4/9.
     */
    public class FutureTaskTest {
    
        private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    
        public static void main(String []args) {
            Callable callable = new AccCallable(1, 2);
            FutureTask futureTask = new FutureTask(callable);
            executorService.execute(futureTask);
            System.out.println("go to do other things in main thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("go back in main thread");
            try {
                int result = (int) futureTask.get();
                System.out.println("result is " + result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
        static class AccCallable implements Callable<Integer> {
            private int a;
            private int b;
            public AccCallable(int a, int b) {
                this.a = a;
                this.b = b;
            }
    
            @Override
            public Integer call() throws Exception {
                System.out.println("acc a and b in threadId = " + Thread.currentThread().getName());
                return a + b;
            }
        }
    }
    

    输出结果为:

    go to do other things in main thread
    acc a and b in threadId = pool-1-thread-1
    go back in main thread
    result is 3
    

    实现分析

    ​ 在分析实现前,我们先想下如果让我们实现一个类似FutureTask的功能,我们会如何做?因为需要获取执行结果,需要一个Object对象来存执行结果。任务执行时间不可控性,我们需要一个变量表示执行状态。其他线程会调用get方法获取结果,在没达到超时的时候需要将线程阻塞或挂起。因此需要一个队列类似的结构存储等待该结果的线程信息,这样在任务执行线程完成后就可以唤醒这些阻塞或挂起的线程,得到结果。FutureTask的实际实现也是类似的逻辑,具体如下。

    ​ 首先看下FutureTask的主要成员变量如下:

    //futureTask执行状态
    private volatile int state;
    //具体的执行任务,会在run方法中抵用callable.call()
    private Callable<V> callable;
    //执行结果
    private Object outcome; 
    //获取结果的等待线程节点
    private volatile WaitNode waiters;
    

    对于执行状态,在源码中已经有了非常清晰的解释,这里我只是贴出源码,不在进行说明,具体如下:

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

    然后我们看下FutureTask的构造函数,如下:

    public FutureTask(Callable<V> callable) {
            if (callable == null)
                throw new NullPointerException();
            this.callable = callable;
            this.state = NEW;       // ensure visibility of callable
    }
    public FutureTask(Runnable runnable, V result) {
            //构造函数传入runnable对象时调用静态工具类Executors的方法转换为一个callable对象
            this.callable = Executors.callable(runnable, result);
            this.state = NEW;       // ensure visibility of callable
    }
    

    如前所述,FutureTask的执行线程中会调用其run()方法执行任务,我们看下这块逻辑:

    public void run() {
            //1.如果执行状态不是NEW或者有其他线程执行该任务,直接返回
            if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                             null, Thread.currentThread()))
                return;
            try {
                Callable<V> c = callable;
                //2.如果执行状态是NEW,即任务还没执行,直接调用callable.call()方法获取执行结果
                if (c != null && state == NEW) {
                    V result;
                    boolean ran;
                    try {
                        result = c.call();
                        ran = true;
                    } catch (Throwable ex) {
                        result = null;
                        ran = false;
                        //3.发生异常,更新status为EXCEPTIONAL,唤醒挂起线程
                        setException(ex);
                    }
                    //4.如果结果成功返回,调用set方法将设置outcome,更改status执行状态,唤醒挂起线程
                    if (ran)
                        set(result);
                }
            } finally {
                // runner must be non-null until state is settled to
                // prevent concurrent calls to run()
                runner = null;
                // state must be re-read after nulling runner to prevent
                // leaked interrupts
                int s = state;
                if (s >= INTERRUPTING)
                    handlePossibleCancellationInterrupt(s);
            }
        }
    

    我们看下set函数的实现,具体看下4中的执行:

     protected void set(V v) {
            //将执行状态变更为COMPLETING
            if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
                //设置执行结果
                outcome = v;
                //设置执行状态为NORMAL
                UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
                //执行完成后处理操作,具体就是遍历阻塞链表,删除链表节点,并唤醒每个节点关联的线程
                finishCompletion();
            }
        }
    

    以上就是任务执行线程做的逻辑,以上逻辑也回答了FutureTask如何得到执行结果的疑问。下面我们看下用户调用get方法获取执行结果时的实现逻辑,这个时候FutureTask可能处理各种状态,即可能没有执行,执行中,已完成,发生异常等,具体如下:

    public V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException {
            if (unit == null)
                throw new NullPointerException();
            int s = state;
            //执行状态是NEW或者COMPLETING时执行awaitDone将线程加入等待队列中并挂起线程
            if (s <= COMPLETING &&
                (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
                throw new TimeoutException();
            //根据执行状态status进行结果封装
            return report(s);
    }
    
    //我理解这块是get的核心逻辑
    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;
                }
                //如果执行状态是正在执行,说明线程已经被加入到等待队列中,放弃cpu进入下次循环(真正的自旋)
                else if (s == COMPLETING) // cannot time out yet
                    Thread.yield();
                //第一次进入循环,创建节点
                else if (q == null)
                    q = new WaitNode();
                //将节点加入到等待队列中,waiters相当于头阶段,不断将头结点更新为新节点
                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);
            }
        }
    

    这里需要说明一点,FutureTask中的阻塞队列新加入的节点都在头结点并且next指向之前的头结点,waitars指针总是指向新加入节点,通过waitars可以遍历整个等待队列,具体截图如下。此外等待队列节点结构很简单成员变量只有线程引用和next指针,这里再列出器接口。


    futureTask等待队列.png

    读到这里,相信大家已经对FutureTask的实现细节有了一定的认识。此外,FutureTask没有使用锁而是使用Unsafe的是CAS的原子操作来解决竞争问题,减少了锁带来的上下文切换的开销,提高了效率。

    原文

    袁琼琼的技术博客,欢迎指针
    http://yuanqiongqiong.cn/2019/04/08/%E8%B0%88%E8%B0%88FutureTask%E7%9A%84%E5%AE%9E%E7%8E%B0/

    相关文章

      网友评论

        本文标题:谈谈FutureTask的实现

        本文链接:https://www.haomeiwen.com/subject/xkhxfctx.html