JDK源码分析 FutureTask源码分析

前言

一般我们创建线程的方式主要有两种,一种是直接继承Thread类,一种是实现Runnable接口。但是这两种方法是有缺陷的,就是无法任务执行完毕后获取执行的结果。怎么办呢? 当然JDK有提供相关的接口,那么就是Callable和Future接口,通过它们就可以实现在任务执行完毕后获取任务的结果。

一、Callable接口

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable接口是一个泛型接口,我们通过调用call()函数就可以执行我们的业务逻辑,而泛型V便是调用call()函数所返回的值。其实和Runnable是很相似的,只是Callable可以返回值或者抛出异常。


二、Future接口

Future接口代表异步计算的结果,通过Future接口提供的方法我们实现查看异步计算是否执行完毕,或者等待执行完毕并获取执行结果等操作。

我们来看一下Future接口的实现:

public interface Future<V> {
	/*试图取消对该任务的执行*/
    boolean cancel(boolean mayInterruptIfRunning);
    /*判断任务是否被取消,如果在任务正常完成之前被取消,则返回true*/
    boolean isCancelled();
    /*判断任务是否已经完成*/
    boolean isDone();
    /*如果有必要,等待计算执行完毕并获取结果*/
    V get() throws InterruptedException, ExecutionException;
    /*如果有必要,定时等待计算执行完毕(可能)并获取结果(如果结果有效)*/
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

我们来看一下:
boolean cancel(boolean mayInterruptIfRunning)参数的意义:
如果参数mayInterruptIfRunning为true,表明执行该任务的线程应该被中断。若为false,则允许正在执行的任务继续执行。
boolean isDone():这里需要注意的是,当前任务正常完成、抛出异常或者被取消,都会返回true。

这里介绍完Callable和Future两个接口,接下来便是本文的核心部分 —— Future唯一实现类FutureTask的源码分析。


三、FutureTask源码分析

3.1 Future继承结构图

在这里插入图片描述
我们可以看到,FutureTask实现了RunnableFutrue接口,而RunnableFutrue接口既继承了Future接口,又继承了Runnable接口,说明FutureTask既能作为Runnable被Thread执行,也能作为Future用来获取Callable的执行结果。

3.2 参数介绍
	/*表示该任务的运行状态*/
	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;

我们来看一下FutureTask的参数介绍:

  1. NEW: 该任务是个新的任务或者还没执行完成的任务,即初始状态
  2. COMPLETING: 任务已经执行完成或者在执行任务的过程中发生异常,但是还未将任务执行的结果或者抛异常的原因保存到outcome字段的时候,状态会从NEW转换成COMPLETING。该状态保持的时间比较短,所以属于一个中间状态
  3. NORMAL: 该任务已经正常执行完毕并且将执行的结果保存到outcome字段中,状态会从COMPLETING转换成NORMAL,该状态属于最终状态
  4. EXCEPTIONAL: 该任务执行过程中发生异常并且将异常原因保存到outcome字段中,状态会从COMPLETING转换成EXCEPTIONAL,该状态属于最终状态
  5. CANCELLED:任务还未开始执行或者已经开始执行但是还没有执行完成的时候,用户调用cancel(false)函数取消该任务但是不中断任务执行线程,这个时候状态会从NEW转化成CANCELLED。该状态为最终状态
  6. INTERRUPTING:任务还未开始执行或者已经开始执行但是还没有执行完成的时候,用户调用cancel(ture)函数取消任务并且中断任务执行线程,但是还未开始执行中断任务执行线程之前,状态会从NEW转化成INTERRUPTING状态。该状态属于中间状态。
  7. INTERRUPTED :调用interrupt()中断任务执行线程之后状态会从INTERRUPTING转换到INTERRUPTED。这是一个最终状态。

FutureTask的状态转换主要有四条路线:
在这里插入图片描述
白色代表初始状态,黄色代表中间状态(临时状态),绿色则代表最终状态。

FutureTask的其他参数

	/*表示执行的业务逻辑*/
    private Callable<V> callable;
    /*当任务执行完毕后,任务返回的执行结果*/
    private Object outcome; // non-volatile, protected by state reads/writes
    /*任务执行的线程*/
    private volatile Thread runner;
   	/*
   		当任务还没有执行完毕,用户调用get()方法时会被阻塞,被封装成WaitNode结点加入waiters中
		waiters本身是一个单链表形式的存在
	*/
    private volatile WaitNode waiters;

3.3 构造函数
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;  
    this.state = NEW;    //当前任务的初始状态为NEW
}

该构造函数很简单,直接传入Callable实例对象,并设置该任务的初始状态为NEW。

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       
}
/*调用Executors的callable将当前Runnable和result封装成一个Callable对象*/
public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new Executors.RunnableAdapter<T>(task, result);
}
/*采用适配器的设计模式*/
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

该构造函数会把传入的runnable和result封装成一个Callable对象保存在callable字段中。当任务执行完成时,会返回传入的result值。

3.4. FutureTask的API源码分析
3.4.1. run():

前面说道,FutureTask可以被当成一个Runnable被执行,那么来看一下run()方法的具体实现。

public void run() {
	/**
	 * 1. 如果当前任务的执行状态不为NEW,说明当前任务已经执行过了或者被取消了,则直接返回,不需要继续执行
	 * 2. 如果当前任务的执行状态为NEW,通过CAS尝试将当前线程和成员变量runner绑定,若失败,说明runner已经和其他
	 * 线程绑定了,那就直接返回
	 */
    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) {
            	/*call()执行过程中抛出异常*/
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        /*若任务是被中断,那么执行中断处理*/
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

/*等待该任务状态从INTERRUPTING转换成最终状态INTERRUPTED*/
private void handlePossibleCancellationInterrupt(int s) {
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield();
}

来梳理一下run() 的执行流程:

  1. 判断该任务的状态是否为NEW,不为NEW说明该任务已经被执行过或者被取消了,那么直接退出。
  2. 如果该任务的状态为NEW,利用CAS尝试将成员变量runner和当前线程绑定,若失败,说明runner已经和其他的线程绑定了,那么直接退出。
  3. 若该任务状态为NEW并且runner绑定当前线程成功,再次检验该任务的状态,若为NEW,则执行任务。
  4. 执行任务过程若发生异常,则采用setException函数来设置异常。
  5. 若执行成功且一切正常,则采用set函数来设置结果。

我们来看一下run函数中调用的其他函数:

setException

protected void setException(Throwable t) {
	/*采用cas将当前任务的状态从NEW转换成COMPLETING*/
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    	/*设置输出*/
        outcome = t;
        /*设置当前任务的状态为最终态EXCEPTIONAL*/
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

setException函数的流程:

  1. 调用CAS将当前任务的状态从NEW转换成COMPLETING
  2. 将异常原因保存在outcome字段中,outcome字段是用来保存任务执行结果或者异常原因的。
  3. 调用unsafe类将当前任务的状态设置为最终态EXCEPTIONAL
  4. 调用finishCompletion()函数,该函数等分析完set函数再说。

set

protected void set(V v) {
	/*采用cas将当前任务的状态从NEW转换成COMPLETING*/
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        /*设置当前任务的状态为最终态NORMAL*/
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

set函数的流程:
5. 调用CAS将当前任务的状态从NEW转换成COMPLETING
6. 将任务执行的结果保存在outcome字段中。
7. 调用unsafe类将当前任务的状态设置为最终态NORMAL
8. 调用finishCompletion()函数。

setExceptionset的流程很相似,只是输出结果和任务的最终状态有差异而已。

接下来我们来看一个和run方法差不多的:

protected boolean runAndReset() {
    if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                    null, Thread.currentThread()))
        return false;
    boolean ran = false;
    int s = state;
    try {
        Callable<V> c = callable;
        if (c != null && s == NEW) {
            try {
                c.call(); // don't set result
                ran = true;
            } catch (Throwable ex) {
                setException(ex);
            }
        }
    } finally {    
        runner = null;
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    return ran && s == NEW;
}

这个函数和run()函数十分相似,唯一的不同就是当前函数正常执行完毕后不会调用set方法设置执行结果。看源码的解释是该方法是用来执行那些不止执行一次的任务


3.4.2 get()

任务发起线程可以通过调用get()方法来获取任务执行的结果,如果此时任务已经执行完毕,那么会直接返回结果。若任务还未执行完毕,那么调用方会阻塞直到任务执行完毕返回结果为止。

public V get() throws InterruptedException, ExecutionException {
	/*获取该任务的运行状态*/
    int s = state;
    /*判断任务是否执行完毕,记得,COMPLETING只是一个中间状态,不算真正的执行完毕*/
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

get方法的流程如下:

  1. 判断任务是否执行完毕,如果没有,调用awaitDone进行阻塞等待。
  2. 如果执行完毕,调用report方法返回结果。

我们来看一下awaitDone的源码实现:

我们先来看一下FutureTask的内部类WaitNode:

static final class WaitNode {
	/*结点绑定的线程*/
    volatile Thread thread;
    /*当前结点的后继结点*/
    volatile FutureTask.WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

WaitNode是一个单向链表的结构,内部设置thread字段来绑定被阻塞的线程。如果任务还未执行完成,那么调用方的线程会被封装成一个WaitNode结点加入等待队列中。

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
    /*若是定时阻塞等待, 则设置超时时间*/
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    FutureTask.WaitNode q = null;
    boolean queued = false;
    for (;;) {
    	/*如果线程被中断,如果被中断,则调用removeWaiter在等待队列中删除该结点并抛出异常*/
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        int s = state;
        /*如果当前的状态大于COMPLETING,要么任务正常执行完毕,或者任务被取消,或者执行过程中抛出异常*/
        if (s > COMPLETING) {
        	/*将等待队列相应结点所绑定的线程设置为null,并且返回当前任务的状态*/
            if (q != null)
                q.thread = null;
            return s;
        }
        /*如果当前任务的状态处于中间状态COMPLETING,说明任务已经结果但是还未给outcome字段
        赋值,那么这个时候当前线程调用Thread.yield()(可能)转让执行权 交给其他线程优先执行*/
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        /*如果任务还未开始执行完毕且q为null,那么构建一个WaitNode结点*/
        else if (q == null)
            q = new FutureTask.WaitNode();
        /*如果当前结点还未加入等待队列中,那么通过cas的方式将当前结点加入等待队列中(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);
    }
}

梳理一下awaitDone的执行流程:

  1. 判断调用get()方法的线程是否被其他线程中断,如果被中断,那么调用removeWaiter方法从等待队列中移除当前结点并且抛出中断异常。
  2. 判断任务是否执行完毕(前面我们讲过,只要状态大于COMPLETING,都算任务执行完毕),如果是,那么将当前结点绑定的线程置为空并返回结果。
  3. 判断线程是否处于中间状态COMPLETING,如果是,那么调用Thread.yield()转让执行权,让其他线程优先执行。
  4. 判断当前结点是否创建,若没有,则创建一个WaitNode结点。
  5. 判断新建结点是否加入等待队列中,若没有,采用CAS和头插法将结点加入到等待队列中。
  6. 将线程挂起。

以上过程会一直循环执行,直至线程被中断或者任务执行完毕。

我们来看一下结点是如何移除的。

private void removeWaiter(FutureTask.WaitNode node) {
	/*若结点创建*/
    if (node != null) {
        /*将结点绑定线程置为空*/
        node.thread = null;
        retry:
        for (;;) {          // restart on removeWaiter race
            for (FutureTask.WaitNode pred = null, q = waiters, s; q != null; q = s) {
                s = q.next;
                /*在等待队列中,thread为空的结点都为废弃结点*/
                if (q.thread != null)
                    pred = q;
                else if (pred != null) {
                    pred.next = s;
                    /*这里会再检测pred是否已经成为废弃结点,如果是,那么就重新开始遍历*/
                    if (pred.thread == null) // check for race
                        continue retry;
                }
                /*调用cas替换头结点,设置设置失败,重新开始遍历*/
                else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                        q, s))
                    continue retry;
            }
            break;
        }
    }
}

我们看到,removeWaiter会不断遍历整个等待队列,移除thread字段为null(废弃的结点)的结点。

get(long timeout, TimeUnit unit)和get函数差不多,这里就不再赘述。


3.4.3. cancel

接下来我们来分析一下是如何取消任务的。

public boolean cancel(boolean mayInterruptIfRunning) {
	/*判断任务是否还处于NEW状态,若处于,根据mayInterruptIfRunning的值来将当前任务转换成
	INTERRUPTING状态或者CANCELLED状态*/
    if (!(state == NEW &&
            UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                    mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
    	/*如果mayInterruptIfRunning为true,则将任务进行中断*/
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
            	/*调用unsafe类将当前任务的状态设置为INTERRUPTED,最终状态*/
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

cancel执行流程如下:

  1. 判断任务是否还处于NEW状态,若是,根据mayInterruptIfRunning的值来将当前任务转换成
    INTERRUPTING状态或者CANCELLED状态。
  2. 如果mayInterruptIfRunning为true的话,那么将任务执行线程进行中断,并且将该任务的状态设置为INTERRUPTED。
  3. 调用finishCompletion()函数。

需要注意的是,执行cancel(true)方法并不会立马让正在执行的任务停止,而是设置中断标记为true。至于怎么处理中断,需要看具体代码实现。

接下来我们来看一下finishCompletion(),这个方法在任务执行正常完毕、任务执行过程中抛出异常、或者调用cancel方法取消任务时都出现过。

private void finishCompletion() {
    // assert state > COMPLETING;
    /*遍历等待队列,唤醒所有在等待队列上的线程*/
    for (FutureTask.WaitNode q; (q = waiters) != null;) {
    	/*利用cas将等待队列设置为空*/
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
            	/*获取结点所绑定的线程*/
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    /*唤醒挂起的线程*/
                    LockSupport.unpark(t);
                }
                /*遍历当前结点的后继结点*/
                FutureTask.WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }
	/*子类实现*/
    done();
    callable = null;        // to reduce footprint
}

finishCompletion流程比较简单,遍历链表,唤醒相应的线程,最后将callable设置为空。done()方法在FutureTask中是没有任何实现的,由子类去实现。唤醒的线程会在awaitDone方法中继续执行循环,在这次循环中会立刻返回任务的执行状态。

当任务执行完毕或者从awaitDown方法中返回时,会调用report返回返回执行结果。

private V report(int s) throws ExecutionException {
	/*获取返回结果*/
    Object x = outcome;
    /*如果任务的执行状态为NORMAL,说明是正常执行完毕,那么直接返回执行结果*/
    if (s == NORMAL)
        return (V)x;
    /*任务被取消,抛出CancellationException异常*/
    if (s >= CANCELLED)
        throw new CancellationException();
    /*其他情况,抛出执行异常ExecutionException*/
    throw new ExecutionException((Throwable)x);
}

以上便是FutureTask的全部源码分析了。

Demo1: 异步计算

public class FutureExample {
    static class Task implements Callable<Integer>{
        @Override
        public Integer call(){
            int result = 0;
            try {
                /*模拟耗时业务*/
                Thread.sleep(4000);
                result = 1 + 2;
                System.out.println("子线程执行计算完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long st = System.currentTimeMillis();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        new Thread(futureTask).start();
        /*模拟耗时业务*/
        Thread.sleep(2000);
        int a = 1 + 2;
        Integer result = null;
        try {
            result = futureTask.get(1000, TimeUnit.MILLISECONDS);
            System.out.println("主线程执行计算完毕");
            System.out.println("result: " + (a + result));
            long ed = System.currentTimeMillis();
            System.out.println("time: " + (ed - st) + "ms");
        } catch (TimeoutException e) {
            System.out.println("获取超时");
        }
    }
}

Demo2:并发场景下只有一个线程能执行该异步计算

public class FutureExample {
    static class Task implements Callable<Integer>{
        @Override
        public Integer call(){
            int result = 0;
            try {
                /*模拟耗时业务*/
                Thread.sleep(4000);
                result = 1 + 2;
                System.out.println("子线程执行计算完毕");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return result;
        }
    }
    static CountDownLatch latch = new CountDownLatch(5);
    private static ExecutorService threadPool = Executors.newFixedThreadPool(5);
    @Test
    public void test() throws InterruptedException, ExecutionException {
        FutureTask<Integer> task = new FutureTask<>(new Task());
        for (int i = 0; i < 5; i++) {
            threadPool.submit(task);
        }
        latch.await();
        System.out.println("执行结果: " + task.get());
        System.out.println("执行完毕");
    }
}

执行结果:

子线程执行计算完毕
...(等待)

我们可以看到,我们利用线程池执行五次FutureTask,只出现一次子线程执行计算完毕。

参考:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值