併發程式設計—— FutureTask 原始碼分析

莫那·魯道發表於2019-02-26

1. 前言

當我們在 Java 中使用非同步程式設計的時候,大部分時候,我們都會使用 Future,並且使用執行緒池的 submit 方法提交一個 Callable 物件。然後呼叫 Future 的 get 方法等待返回值。而 FutureTask 是 Future 的一個實現,也是我們今天的主角。

我們就從原始碼層面分析 FutureTask.

2. FutureTask 初體驗

我們一般接觸的都是 Future ,而不是 FutureTask , Future 是一個介面, FutureTask 是一個標準的實現。在我們向執行緒池提交任務的時候,執行緒池會建立一個 FutureTask 返回。

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
複製程式碼

newTaskFor 方法就是建立一個了一個 FutureTask 返回。

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}
複製程式碼
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}
複製程式碼

而執行緒池就會執行 FutureTask 的 run 方法。

那麼,我們看看 FutureTask 的 UML。

image.png

可以看出,FutureTask 實現了 Runnable,Future 。Runnable 就不必說了,一個 run 方法,那 Future 呢?

boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;   
複製程式碼

主要是這 5 個方法撐起了 Future,功能相對而言比較薄弱,畢竟這只是一個 Future ,而不是 Promise。

FutureTask 還有一個內部類,WaitNode ,結構如下:

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}
複製程式碼

看起來是不是和 AQS 的節點似曾相識呢?

FutureTask 內部維護了一個棧結構,和 AQS 的佇列有所區別。

實際上,在之前的版本中,FutureTask 確實直接使用的 AQS ,但是 Doug lea 又對該類進行了優化,優化的目的是 :

主要是為了避免有些使用者在取消競爭期間保留中斷狀態。

而內部依然使用了一個 volatile 的 state 變數來控制狀態,同時使用了一個棧結構來儲存等待的執行緒。

至於原因,當然是 FutureTask 的 get 方法是支援併發的,多個執行緒可以獲取到同一個 FutureTask 的同一個結果,而這些執行緒在 get 的阻塞過程中必然是要掛起自己等待的。

知道了 FutureTask 的結構。我們知道,執行緒池肯定會執行 FutureTask 的 run 方法,所以,我們到他的 run 方法看看。

同時,我們也要看看關鍵方法 —— get 方法。

3. FutureTask 的 get 方法

程式碼如下:

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

首先判斷狀態,然後掛起自己等待,最後,返回結果,程式碼很簡單。

注意:FutureTask 中有 7 種狀態:

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;
複製程式碼

構造時,狀態就是 NEW,當任務完成中,狀態變成 COMPLETING。當任務徹底完成,狀態變成 NORMAL。

我們重點看看 awaitDone 和 report 方法。

awaitDone 方法程式碼:

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;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            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);
    }
}
複製程式碼

上面的方法相對於 JUC 其他的類,還是比較簡單的。需要注意一個點:get 方法是可以併發訪問的,當併發訪問的時候,需要將這些執行緒儲存在 FutureTask 內部的棧中。

簡單說說方法步驟:

  1. 如果執行緒中斷了,刪除節點,並丟擲異常。
  2. 如果字型大於 COMPLETING ,說明任務完成了,返回結果。
  3. 如果等於 COMPLETING,說明任務快要完成了,自旋一會。
  4. 如果 q 是 null,說明這是第一次進入,建立一個新的節點。儲存當前執行緒引用。
  5. 如果還沒有修改過 waiters 變數,就使用 CAS 修改當前 waiters 為當前節點,這裡是一個棧的結構。
  6. 根據時間策略掛起當前執行緒。
  7. 當執行緒醒來後,繼續上面的判斷,正常情況下,返回資料。

再看看 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);
}
複製程式碼

也還是很簡單的,拿到結果,判斷狀態,如果狀態正常,就返回值,如果不正常,就丟擲異常。

總結一下 get 方法:

FutureTask 通過掛起自己等待非同步執行緒喚醒,然後拿去非同步執行緒設定好的資料。

4. FutureTask 的 run 方法

上面總結說,FutureTask 通過掛起自己等待非同步執行緒喚醒,然後拿去非同步執行緒設定好的資料。

那麼這個過程在哪裡呢?答案就是在 run 方法裡。我們知道,執行緒池在執行 FutureTask 的時候,肯定會執行他的 run 方法。所以,我們看看他的 run 方法:

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);
    }
}
複製程式碼

方法邏輯如下:

  1. 判斷狀態。
  2. 執行 callable 的 call 方法。
  3. 設定結果並喚醒等待的所有執行緒。

看看 set 方法是如何設定結果的:

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

先將狀態變成 COMPLETING,然後設定結果,再然後設定狀態為 NORMAL,最後執行 finishCompletion 方法喚醒等待執行緒。

finishCompletion 程式碼如下:

private void finishCompletion() {
    // assert state > COMPLETING;
    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
}
複製程式碼

該方法先將 waiters 修改成 null,然後遍歷棧中所有節點,也就是所有等待的執行緒,依次喚醒他們。

最後執行 done 方法。這個方法是留個子類擴充套件的。FutureTask 中是個空方法。比如 Spring 的 ListenableFutureTask 就擴充套件了該方法。還有 JUC 裡的 QueueingFuture 類也擴充套件了該方法。

如果異常了就將狀態改為 EXCEPTIONAL。

如果使用者執行了 cancel(true)方法。該方法 Java doc 如下:

試圖取消對此任務的執行。如果任務已完成、或已取消,或者由於某些其他原因而無法取消,則此嘗試將失敗。當呼叫 cancel 時,如果呼叫成功,而此任務尚未啟動,則此任務將永不執行。如果任務已經啟動,則 mayInterruptIfRunning 引數確定是否應該以試圖停止任務的方式來中斷執行此任務的執行緒。

也就是說,這個 mayInterruptIfRunning 決定當任務已經在執行了,還要終止這個任務。如果 mayInterruptIfRunning 是 true ,就會先將狀態改成 INTERRUPTING,然後呼叫執行緒的 interrupt 方法,最後,設定狀態為 INTERRUPTED。

在 run 方法的 finally 塊中,對 INTERRUPTING 有判斷,也就是說,在 INTERRUPTING 和 INTERRUPTED 的這段時間,會執行 finally 塊,那麼這個時候,就需要自旋等待狀態變成 INTERRUPTED。

具體程式碼如下:

private void handlePossibleCancellationInterrupt(int s) {
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt
}
複製程式碼

5. 總結

關於 FutureTask 就介紹完了,該類最重要的就是 get 方法和 run 方法,run 方法負責執行 callable 的 call 方法並設定返回值到一個變數中, get 方法負責阻塞直到 run 方法執行完畢任務喚醒他,然後 get 方法回去結果。

同時,FutureTask 為了多執行緒可以併發呼叫 get 方法,使用了一個棧結構儲存所有等待的執行緒。也就是說,所有的執行緒都等得到 get 方法的結果。

雖然 FutureTask 的設計很好,但我仍然覺得使用非同步是更好的選擇,效率更高。

相關文章