簡單聊一聊FutureTask的實現

EumJi發表於2018-02-05

簡介

FutureTask是一種支援取消的非同步任務包裝類,也就是說FutureTask執行的時候不立即返回結果,自己可以通過非同步呼叫get方法獲取結果,也可以中途呼叫cancel方法取消任務。而且必須要知道的就是FutureTask只是任務的包裝類,並不是真正的任務類。

FutureTask實現RunnableFuture介面,而RunnableFuture繼承了Runnable, Future介面。

實現

下面我們將介紹一下FutureTask的具體實現。

FutureTask狀態

正是因為FutureTask只是任務執行的包裝類,所以他肯定是需要很多的狀態來維護任務執行的狀態,不然怎麼能cancel,get呢,下面我們具體來看一下。

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 正常的執行過程,從開始到結束
NEW -> COMPLETING -> EXCEPTIONAL 執行過程中發生了異常
NEW -> CANCELLED 任務被取消
NEW -> INTERRUPTING -> INTERRUPTED 任務被中斷

FutureTask構造方法

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) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}
複製程式碼

很簡單,就是強行把runable包裝成callble物件,並且返回值為傳入的result。

run方法

在FutureTask中有兩種執行的方式,run方法和runAndReset方法,先看一下run方法的實現。

public void run() {
   //不是NEW狀態的任務無法執行
    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; //正常狀態下設定狀態為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.執行任務,如果發生異常,進行異常狀態下的設定,否則正常邏輯狀態設定

3.最後在返回前進行狀態設定,如果處於中斷狀態,設定更新狀態。

異常處理

接下來我們需要看一下異常狀態下的處理。

protected void setException(Throwable t) {
   //嘗試設定到COMPLETING狀態
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t; //設定outcome為異常物件
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // 最終的狀態
        finishCompletion(); 
    }
}
複製程式碼

這裡就滿足了我們在上面表格中介紹到的異常狀態下的狀態的變化過程。發生異常後還需要執行finishCompletion方法,finishCompletion的主要目的其實是喚醒所有等待獲取結果的執行緒,所以我們把放在get方法的後面再講。

正常結束

下面我們看一下set方法的乾的事情,根據我們之前在表格中的介紹應該很容易才出來。

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

set方法邏輯和setException邏輯類似,只是他們設定的最終狀態值和outcome值不同而已。就不多說了。

發生中斷

我們接著看run方法中finally中的方法

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

這裡做的事情很簡單,就是假如發生了中斷的事件,那麼此時就是釋放鎖,一直重試到狀態變成了INTERRUPTED。

runAndReset方法

我們再看一下runAndReset方法和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;
}
複製程式碼

很容易看出,runAndReset在正常執行結束後是不會更改狀態的,這樣的話勢必會造成正常情況下是無法獲取程式的結果的。之所以這麼做也是因為任務是要複用的,因為這個方法是用來做週期迴圈排程的。所以也不會改變狀態,也不會設定結果值。具體的體現我們可以再ScheduleThreadPoolExecutor中具體檢視。

cancel方法

上面我們介紹了中斷時的狀態的改變,但是我們沒介紹到底是怎麼產生中斷的,接下來我們看一下,先說明一下其實中斷和取消的方法是使用同一個方法,只是狀態值不同

public boolean cancel(boolean mayInterruptIfRunning) {
   //mayInterruptIfRunning true代表中斷,false代表取消
   //如果是NEW狀態又執行了中斷或取消,跳過,否則直接結束
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {  
       //中斷的情況,設定執行緒中斷
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}
複製程式碼

我們可以看到,cancel同時具有取消和中斷兩種功能,

1.當我們的任務還是NEW狀態,又改變狀態成功,這說明任務已經無法執行了,設定執行緒狀態,如果不處於NEW狀態,或者修改狀態失敗則直接結束方法。

2.不滿足的情況下就會判斷任務是否為中斷,如果中斷的話就把執行緒的狀態也設定為中斷,並改變最終的狀態。

3.最終也都要釋放等待的執行緒(具體留在後面說明)。

get方法

get方法主要有兩種,一種是一直等待,一種是設定超時的時間。

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

如果任務的狀態還小於COMPLETING,說明任務還沒有完成,不管是有沒有發生意外的情況。此時都要把獲取結果的執行緒加入到等待結果的連結串列中,如果是已完成則直接獲取結果,很簡單就不多描述了。

我們在繼續看看帶超時時間的get方法

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

其實拋開awaitDone方法都是一致的,所以我們直接來看一下awaitDone方法。

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) //說明馬上狀態就改變了,那麼此時肯定不會入隊了,所以讓出時間片
                Thread.yield();
            else if (q == null) //說明還在NEW狀態,
                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);
        }
    }
複製程式碼

好,這裡的所及還算是比較複雜,在這裡我們簡單的總結一下

1.判斷執行緒是否有被中斷,如果被中斷了直接結束等待。否則

2.判斷state的狀態是否COMPLETING,如果大於說明要麼執行完要麼出了狀況,可以去拿值了。直接返回,如果不滿足

3.在判斷是否為COMPLETING,也說明該執行的也執行了,現在在修改狀態中,馬上就可以拿值,所以放棄時間片,等下次來再判斷。如果不滿足

4.否則沒有入隊就入隊,否則如果是設定超時時間,就判斷是否已經超時,超時就移除,否則就把執行緒掛起指定時間。

說了那麼多,很多方法都呼叫了finishCompletion方法,都需要釋放等待結果的執行緒,接下來我們就一起看看其中的邏輯。

finishCompletion方法

其實實現的邏輯還是很簡單的,

private void finishCompletion() {
        // 處理等待的執行緒連結串列
        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
    }
複製程式碼

這裡的邏輯也是非常簡單就是喚醒所有等待的執行緒,如果還沒處理完畢,又會被掛起。

總結

本文從run方法,get方法,cancel方法以及他們所涉及到的方法總體上弄清楚了FutureTask的功能。

1.FutureTask只是任務的包裝類,真正的執行的邏輯不在其中。

2.一定要弄清楚FutureTask的幾種狀態值,非常重要。

3.只有NEW狀態的任務才能被執行,run方法執行後正常情況下會改變state的值,而runAndReset不會,因為兩種方法的場景不同。

4.runAndReset排程任務,發生異常任務就會終止後面的排程。

目前FutureTask主要是用於執行緒池中,用於非同步獲取執行結果和執行緒池的排程上。

最後

此文章都是個人的理解並整理,如果存在什麼邏輯上的疏漏或者表述不當,歡迎吐槽反饋!!

與君共勉!!!

相關文章