Java Future的實現原理

前陣子在用C++ 98(是比較落后了,嗯,C++11原生支持Future)開發的時候,對脫離業務的公共邏輯抽象出來了一個簡單的任務執行框架,里面主要是線程池和一些同步異步的任務。在開發異步任務的時候,為了實現類似java Future模式的能力,對實現方式考量了好久,最終使用了信號量這么重的東西來實現了Future的能力,同時也不禁對java的Future實現產生興趣,java的Future是怎么實現的呢?

concurrent 包

java Future相關的代碼基本都在java.util.concurrent的包里面,Future.java是一個接口,定義了最基本的一些任務操作和狀態判斷。

// Future.java

boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit);

我們從FutureTask.java去了解Future的內部機制,FutureTaskFuture有比較接地氣的實現,其他的實現或多或少都加入了新的一些特性,對了解原理沒太大幫助。

FutureTask.java是對FutreRunnable最簡單的實現,所以FutureTask是一個可執行的異步對象。

FutureTask的狀態

FutureTask定義了7種狀態,這7種狀態里面包含了簡單的狀態機,使用了一個用volatile修飾的int來記錄狀態。如下:

/** 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;

狀態的變換基本在FutureTask.java的所有函數中都有體現,我們來看看幾個典型的狀態變換:

  1. 構造函數

    構造函數完成時,將狀態置為NEW

  2. 取消操作

    取消操作對任務狀態進行判斷。

    1. 如果任務正在執行但沒有完成時,發出中斷,并將任務狀態置為中斷中狀態,并在執行線程完成后,置為中斷完成狀態NEW -> INTERRUPTING -> INTERRUPTED

    2. 當任務還沒有執行,則直接置為取消狀態 NEW -> CANCELLED

  3. 執行操作

    執行分執行成功和執行異常兩種,狀態轉換路徑是這兩個。

    NEW -> COMPLETING -> NORMALNEW -> COMPLETING -> EXCEPTIONAL

FutureTask的執行

FutureTask因為封裝了Runnable的接口,實現了run函數,所以可以直接執行,直接執行使用主線程;FutureTask的另外一種執行方式是提交到線程池去執行,由線程池去分配執行線程;只有提交到線程池去執行才能體現異步的特性。不過我們不關注執行的方式,我們關注執行的邏輯。

FutureTask的構造函數傳入了CallableRunnable對象,也即是需要執行的業務邏輯,他是業務邏輯的基本表現形式,保存在類屬性callable,在run函數里面,調用callalbe.call()來執行業務邏輯。run函數主要完成以下幾個操作。

  1. 判斷當前狀態是否為NEW,如果不是,說明任務被執行過,或者已被取消,直接返回。
  2. 如果狀態為NEW,接著會通過unsafe類把任務執行線程保存在runner字段中,如果保存失敗,則直接返回
  3. 執行任務
  4. 任務執行成功,set保存結果,不成功setException保存異常信息,任務的狀態在這里改變。

以下是run函數的邏輯。

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread())) // 1, 2
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call(); // 3
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex); // 4
            }
            if (ran)
                set(result); // 4
        }
    } 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);
    }
}

由于在任務執行的過程中,可能被取消,所以在finally塊里,會任務的根據狀態來做一些善后的工作。

FutureTask的取消

任務的取消比較簡單,我們知道,在執行的時候,執行任務的線程會保存在runner屬性中,所以對于正在執行的任務,取消的本質就是將執行的線程取出來,向該線程發出interupt信號。但對于一個較為完備的取消動作,cancel做了一下幾個動作。

  1. 判斷任務當前執行狀態,如果任務狀態不為NEW,則說明任務或者已經執行完成,或者執行異常,不能被取消,直接返回false表示執行失敗。

  2. 判斷需要中斷任務執行線程,則

    1. 把任務狀態從NEW轉化到INTERRUPTING。這是個中間狀態。
    2. 中斷任務執行線程。
    3. 修改任務狀態為INTERRUPTED。
  3. 如果不需要中斷任務執行線程,直接把任務狀態從NEW轉化為CANCELLED。如果轉化失敗則返回false表示取消失敗。

  4. 調用finishCompletion

    這個函數將阻塞在等待這個任務完成的線程喚醒,具體操作是LockSupport.unpark(t),這些線程都是在awaitDone函數內的LockSupport.park(this)中阻塞的,關于awaitDone函數的作用后文還會繼續介紹。finishCompletion除了在這里有調用以外,在setsetException中也有調用。

    以下是finishCompletion的實現

    /**
     * Removes and signals all waiting threads, invokes done(), and
     * nulls out callable.
     */
    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
    }
    

以下是cancel函數的實現。

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) // 1, 2
        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); // 3
            }
        }
    } finally {
        finishCompletion(); // 4
    }
    return true;
}

FutureTask的結果

異步任務提交到線程池去執行后,由于無法預知什么時候結束,所以必須得提供接口來獲取任務執行的結果,在FutureTask中,get函數用來獲取任務執行的結果。該函數有了設置超時和不超時的兩種實現。

除去超時的差異,get操作對任務的狀態進行判斷,當狀態還沒有完成的時候,調用awaitDone函數來等待完成,我們在上面提到過這個函數,對于未完成的任務awaitDone阻塞,而finishCompletion喚醒阻塞。

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

我們來詳細看看awaitDone函數做了什么操作,它是如何阻塞的,它是怎么設置超時和完成返回的。

awaitDone主題是一個死循環,輪詢判斷任務的狀態。

  1. 當執行的線程被中斷時,調用removeWaiter移除等待節點WaitNode,拋出中斷異常

  2. 當狀態為已經完成,直接返回

  3. 當狀態為完成中,通過Thread.yield()讓出CPU時間

  4. 如果當前線程還沒有創建WaitNode等待節點保存到等待隊列里面去,則新建一個等待節點,插入到等待鏈表,表明當前線程也準備進入等待該任務完成的隊列中去。

  5. 最后是進入阻塞的動作,通過LockSupport.park,如果設置了超時的時間,則將時間作為參數傳遞到park中去。

    綜上,awaitDone函數除了對狀態的判斷以外,核心就是LockSupport的阻塞等待完成的操作了。我們后面還會探討一下LockSupport。

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

awaitDone的阻塞完成以后,就會將結果返回,將結果返回是通過report函數來實現的,返回的是執行完成的結果或者是執行中獲得的異常信息。

LockSupport

異步執行任務,在獲取任務狀態時,阻塞是必然的,在開頭引子我簡單的提到了在自己實現的那個簡陋的框架內,使用了信號量的方式來設置異步任務的阻塞和執行狀態。在這里是通過LockSupport來實現的,阻塞(park)和喚醒(unpark)。

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

可以看到LockSupport則是通過UNSAFE的同名函數來實現的。java不能直接訪問操作系統底層,而是通過本地方法來訪問。Unsafe類提供了硬件級別的原子操作。

Unsafe類是在sun.misc包下,不屬于java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基于Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運行效率,增強Java語言底層操作能力方面起了很大的作用。Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,因此Java官方并不建議使用的,官方文檔也幾乎沒有。

所以在這里java也是使用了C級別的線程同步機制來完成這些操作的,在這就不再展開了。


為保持文章內容的連貫性,部分內容參考自:

  1. https://www.skyreal.me/future-task-yuan-ma-jie-xi/
  2. http://beautyboss.farbox.com/post/study/shen-ru-xue-xi-futuretask

原文鏈接

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容