Java并发:Future机制

52 阅读11分钟

大家好,我是茄子。今天讲讲Future。

JDK环境:1.8.0_251,系统环境:win10

前言

本文编写思路是先使用一下Future机制,然后探究各个方法的底层原理,最后总结一下。

看这篇文章需要了解的内容有:Unsafe、LockSupport、CAS、volatile

为什么要有 Future 机制?

通过实现 Runnable 接口创建多线程时,Thread 类的作用就是把 run() 方法包装成线程执行体。那么是否可以直接把任意方法都包装成线程执行体呢?Java以前不行!但 Java 的模仿者 C# 可以( C# 可以把任意方法包装成线程执行体,包括有返回值的方法)。

也许受此启发,从 Java 5 开始,Java 提供了 Callable 接口,Callable 接口提供了一个 call() 方法可以作为线程执行体,但 call() 方法比Runable 的 run() 方法功能更强大。

  • call() 方法可以有返回值。
  • call() 方法可以声明抛出异常。

因此,我们完全可以提供一个 Callable 对象作为 Thread 的 target,而该线程的线程执行体就是该 Callable 对象的 call() 方法。问题是:Callable 接口是 Java 5 新增的接口,而且它不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为 Thread 的 target。而且 call() 方法还有一个返回值—— call() 方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取 call() 方法的返回值呢?Java 5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该实现类实现了 Future 接口,并实现了 Runnable 接口——可以作为 Thread 类的 target。

简单使用

接下来我们简单使用一下

public class Calculate {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        Future<Integer> future = executorService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                        int sum = 0;
                        for (int i = 1; i <= 100; i++) {
                                Thread.sleep(10);
                                sum += i;
                        }
                        return sum;
                }
        });

        System.out.println("主线程等待计算结果。。。");
        System.out.println("计算结果是:" + future.get());

        // 关闭线程池
        executorService.shutdown();
    }
}

计算的时候模拟耗时,主线程会阻塞直到获取结果为止。这里的计算只是一种操作。实际业务中可能是更复杂的计算或远程请求等等。

FutureTask源码解析

先看一下 FutureTask的成员变量

// 任务状态
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;

// callable对象,执行完后置空
private Callable<V> callable;
// 要返回的结果或要引发的异常来自 get() 方法
private Object outcome; // non-volatile, protected by state reads/writes
// 执行Callable的线程
private volatile Thread runner;
// 等待线程的一个链表结构
private volatile WaitNode waiters;

可以注意到 state、runner、waiters 都是用 volatile 关键字修饰的。所以它们的变化对其他线程可见。

submit方法

//java.util.concurrent.AbstractExecutorService
public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
    //...
}

当我们在线程池中执行 Callable 类型的任务的时候,其实就是封装成一个 FutureTask 对象去执行。FutureTask实现了RunableFuture接口,这个RunableFuture接口继承了Runable和Future接口。

classDiagram
    class Runnable {
        <<interface>>
    }
    class Callable {
        <<interface>>
    }
    class Future {
        <<interface>>
    }
    class RunnableFuture {
        <<interface>>
    }
    class FutureTask{
    }
    Runnable <|-- RunnableFuture
    Future <|-- RunnableFuture
    RunnableFuture <|.. FutureTask
    Callable <|.. FutureTask

run方法

public void run() {
    // 只有当状态为新建且当前FutureTask无线程执行的时候才可以继续。
    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 {
                // 执行call方法,并获取返回结果
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                // 如果执行过程出现异常,则将异常赋值到outcome上
                setException(ex);
            }
            //如果正常执行完成,则将result赋值到outcome上
            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 方法。异常会走 setException 方法。

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
        finishCompletion();
    }
}

如果任务没有被取消,将 future 执行完的返回值赋值给 result 结果。FutureTask 任务的执行状态是通过 CAS 的方式进行赋值的,并且由此可知,COMPLETING 其实是一个瞬时状态。当将线程执行结果赋值给 outcome 后,状态会修改为对应的 NORMAL,即正常结束。

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); 
        finishCompletion();
    }
}

过程同上类似,将异常赋值给 outcome 后,状态会修改为对应的 EXCEPTIONAL。

get方法

 public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    if (s <= COMPLETING &&
        (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
        throw new TimeoutException();
    return report(s);
}

调用 get 方法的时候,如果状态小于 COMPLETING,就会通过awaitDone方法等待。带超时时间的get方法,如果在指定时间后,状态还是小于 COMPLETING,说明任务超时,抛出异常。

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;
        }
        // 准备完成,通过yield方法再等等
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            // 如果q为空,说明等待队列中没有节点,那么就创建第一个节点
            q = new WaitNode();
        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);
    }
}

可以看到如果设置截止日期,那么会判断到截止日期前的时间,如果时间到了,就把等待节点从等待队列中移除,并返回state;如果时间没到就会再阻塞一段时间,再循环判断;如果没有设置截止日期,等待结果的线程就会一直阻塞。那么当任务执行完成会发生什么呢?还记得我们提到过在 run 方法中如果正常完成会执行 set 方法吗?我们再回头看一下。

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

可以看到在 set 方法中执行了 finishCompletion 方法

 private void finishCompletion() {
    // 循环等待队列,将节点中的线程依次通过unpark命令唤醒。
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            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; // unlink to help gc
                q = next;
            }
            break;
        }
    }
    // 模板方法,交给子类实现
    done();

    callable = null;        // to reduce footprint
}

可以看出 finishCompletion 方法中会把在 awaitDone 方法中等待结果的线程统统唤醒(unpark),因为需要的结果出来了,可以拿结果去做其他事情了。等待结果的线程再下一次循环的时候发现 s > COMPLETING,说明任务执行完成了(不管有没有问题),就把 s 给返回了。然后就会执行 report 方法了。

report方法

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

report 方法的逻辑很简单,通过状态进行逻辑的判断,状态为 Normal 说明任务正常执行完成,将结果返回;被取消就抛出被取消的异常;否则就抛出执行中遇到的异常,比如空指针异常、算术运算异常。

setException 方法和 set 方法 思路一致,就不再赘述了。

cancel 方法

Future接口提供了cancel方法,允许取消任务。

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

我们可以在取消任务的时候通过参数来决定是否让线程中断。线程中断不代表立刻停止线程,而是修改线程的中断状态标识(中断状态由false变成true),由线程自行决定如何处理。

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

这段代码核心逻辑是让清除等待队列中 thread 为 null 的节点。队列中存在一个节点和这个 node 是同一个引用。node 的 thread 置为空了,队列中也就能找到,找到后用后一个节点覆盖前一个节点,完成删除。

首先 q 指向队列的头节点,s 指向 q 的下一个节点,pred 指向 q 的上一个节点。如果 q 的 thread 不为空,就赋值给 pred,q 继续向后走。如果 q 为空,说明 q 节点是需要移除的,那么就用后一个节点覆盖这个节点(pred.next = s)。

但也会遇到一个问题,就是向后遍历的过程中,别的线程因为中断或超时也执行了 removeWaiter 方法,而 node 正好又是和这里的 pred 是同一个引用,那么就需要重新遍历整个等待队列了(retry)。

remoteWrite1.jpg

如果队列的第一个节点正好 thread 为空,此时 q 是头节点,pred 为空,s 是头节点的下一个节点,那么就可以使用 s 替换头节点,所以在第16行可以看到CAS。

remoteWrite.jpg

再谈 run 方法

在 run 方法中的 finally 代码块中有这两行代码

 public void run() {
    // ...
    try {
       // ...
    } finally {
        
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

为什么要有 runner = null 这行代码呢? 在 run 方法入口处,只有 runner 为空时,才能抢锁进入,防止并发执行。任务执行执行完成了,runner 字段可以为空了,不然引用着一个线程,可能会产生内存问题。

handlePossibleCancellationInterrupt方法内容如下:

private void handlePossibleCancellationInterrupt(int s) {
   
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield();
}

为什么要执行 handlePossibleCancellationInterrupt 方法呢?

主要是处理因cancel方法引起的中断。

cancel 方法可以选择传入true表示,如果任务还在运行那么调用运行任务线程的 interrupt 方法进行中断,如果是调用 cancel 的线程还没有完成中断那么当前运行的线程会让步。为什么这么做?我们上面说到过,A线程运行任务,B线程cancel任务,B中断线程A其实是需要时间的,B会先修改任务状态为 INTERRUPTING,然后中断线程A,然后修改状态为 INTERRUPTED 并唤醒等待的线程,从INTERRUPTING - > INTERRUPTED 这段时间,线程A只需要让出cpu等待即可,就不用浪费cpu啦。

通过代码中最初的注释可以看出,设计者的想法是清除掉执行线程的中断标识,但是又认为有中断标识可以用作通信,所以只是安静地让出cpu,等待状态的改变。

总结

Future机制的基本原理

基于Future机制的异步编程模型可以分为以下几个步骤:

  1. 创建一个 Future对象,并把异步任务提交给一个线程池进行处理。
  2. 主线程立即返回,并可以继续执行其他操作。
  3. 线程池中的线程进行异步计算,直到计算完成。
  4. Future对象可以通过调用 get() 方法获取异步计算的结果,或者捕获由 get() 抛出的异常来获取异常信息。 对于阻塞的操作来说,Future.get() 方法非常重要。它可以用来阻塞主线程,直到异步操作的结果返回为止。如果异步操作在主线程调用 Future.get() 方法之前已经完成,那么调用 Future.get() 方法会立即返回异步操作的结果;如果异步操作还没有完成,那么调用 Future.get() 方法会等待异步操作完成,并返回异步操作的结果。

Future机制的阻塞与唤醒原理

在 awaitDone 方法中,如果线程没有被中断,任务的状态还是 NEW,那么第一次循环创建一个WaitNode,WaitNode有当前线程的引用。第二次循环会加入等待队列。第三次循环会通过LockSupport的park方法阻塞自己。等removeWaiter 或者 finishiCompletion 方法执行的时候,会从 WaitNode 中拿到线程的引用,再调用unpark方法唤醒。

参考材料