Android FutureTask 分析

SimplePeople發表於2017-06-15

之前在研究AsyncTask原始碼的時候發現了它的內部使用了FutureTaskFutureCallable類來實現,因為之前在學習Java的時候並沒有接觸到這些東西,於是乎就開啟了百度看了半天別人的部落格也沒有理解其用法以及原理,後來果斷的檢視了一下其原始碼之後才知道其來龍去脈。官方文件這麼介紹FutureTask類的。

A cancellable asynchronous computation. This class provides a base implementation of Future, with methods to start and cancel a computation, query to see if the computation is complete, and retrieve the result of the computation. The result can only be retrieved when the computation has completed; the get methods will block if the computation has not yet completed. Once the computation has completed, the computation cannot be restarted or cancelled (unless the computation is invoked using runAndReset()).

翻譯:

這是一個可以取消的非同步計算,該類提供了Future的基本實現,具有啟動和取消運算,查詢運算是否結束,並且檢查返回計算的結果,該結果只能在執行完成之後才能獲取到,如果程式沒有執行結束,則`get()`將會阻塞。程式執行結束之後,無法重新啟動或者是取消程式(除非呼叫`runAndReset`方法)

也就是說FutureTask也是一個用來執行非同步任務的類,同時當程式執行完成之後還會返回運算的結果。我們之前也學過了使用Thread+Runnable來執行非同步任務的,但是使用這種方式不能獲取到執行的結果而已。下面我們就來看看裡面具體的原理。

用法

我們就使用該類來實現一個模擬一個非常簡單的事情,使用Thread.sleep()來模擬執行耗時的操作工作,然後將執行完成的結果返回出來。然後列印出來:

public class FutureTaskActivity extends Activity implements View.OnClickListener {

    .........

    //建立一個實現Callable介面的並且在 call()方法中做耗時的操作
    class WorkTask implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            //我們這裡通過使用執行緒 sleep來模擬耗時的操作,以後我們所有的耗時操作都在該方法裡面執行了
            Thread.sleep(5000);
            //將執行的結果返回出去
            return 1000;
        }
    }

    .........

    private void executeTask() {
        //建立一個worktask,並且當作引數傳入到FutureTask中。
        WorkTask workTask = new WorkTask();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(workTask) {
            @Override
            protected void done() {
                try {
                    //該方法回撥意思執行緒執行結束回撥的,然後獲取call方法中返回的結果
                    int result = get();
                    Log.i("LOH", "result..." + result);
                    Thread.currentThread().getName();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        //將FutureTask作為引數傳入到Thread函式中執行。
        Thread thread = new Thread(futureTask);
        //啟動執行緒執行任務
        thread.start();
    }
}

從上面的程式碼中我們可以看到其實它的使用跟Runnable的使用差不多的,只是多出了一個用來實現Callable介面的類作為引數傳入到 FutureTask中,並且以前的那種方法不能監聽執行緒執行完成的。以前我們在使用執行緒的時候都知道只有三種方式來執行非同步任務:

  • 使用new Thread(new Runnable()).start()的方法來執行非同步任務,也就是說將實現Runnable介面的類當作引數傳入到Thread 類中,然後使用thread.start來啟動執行緒執行
  • 通過繼承Thread類並且重寫該類的 run 方法,在該方法中執行耗時的操作,同樣也是使用 Thread.start()來啟動執行緒執行
  • 使用執行緒池來執行實現了 Runnable 介面的類,這樣子也可以達到執行非同步任務的目的。

通過對上面的這些總結我們可以知道了要想實現非同步任務的話就必須實現 Runnable() 介面才行的,所以我們也可以非常肯定的斷定FutureTask也是實現了該介面的。不然就無法放到執行非同步任務的,通過對上面的一個簡單的介紹我們知道了如何使用它,下面就來看看裡面到底是一個什麼樣的機制。

原始碼分析

首先我們通過一個類的關係圖來看看這幾個類之間的關係圖

image_1bhu4a4bccid1s0iq001sgbpe49.png-36.4kB

從圖中我們可以一目瞭然的看到FutureTask實現了RunnableFuture介面,但是RunnableFuture也實現了Runnable介面和Future介面,所以FutureTask可以當作任務線上程池中執行,也可以當作引數傳入Thread中進行啟動任務,在建立FutureTask物件的時候需要傳入一個Callable介面的實現類,從上面的中我們可以看到執行FutureTask任務同樣也是在 run方法中的。

private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long STATE;
    private static final long RUNNER;
    private static final long WAITERS;
    static {
        try {
           //通過Unsafe來獲取欄位state相對於本物件記憶體地址的偏移地址
            STATE = U.objectFieldOffset
                (FutureTask.class.getDeclaredField("state"));
            //獲取欄位runner在記憶體中的偏移地址
            RUNNER = U.objectFieldOffset
                (FutureTask.class.getDeclaredField("runner"));
            //獲取欄位waiters在記憶體中的偏移地址
            WAITERS = U.objectFieldOffset
                (FutureTask.class.getDeclaredField("waiters"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }

        Class<?> ensureLoaded = LockSupport.class;
}

在建立FutureTask物件之前,有一個非常重類 sun.misc.Unsafe 該類的介紹我們這裡也不做介紹,在我的另外一篇部落格中會有介紹的。靜態程式碼塊中的功能主要是獲取該物件的欄位在記憶體中的偏移地址,獲取這些偏移地址的作用是為了直接操作記憶體中某個變數的變數值做準備的。我們在學習C或者是C++的時候知道,如果我們知道了某個物件的某個欄位的記憶體地址話,那我們就可以直接通過地址的方式來更新該欄位的記憶體值了。

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}
public class FutureTask<V> implements RunnableFuture<V> {

   .......

    public void run() {
        /**
         * 如果當前的執行緒的狀態不是新建立的話就返回
         * 第三個條件判斷是 安全檢查具體沒有找到原始碼,我們先不用管
         */
        if (state != NEW ||
            !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
            return;
        try {
            //將我們傳進來的callable物件賦值給一個臨時變數
            Callable<V> c = callable;
            //判斷傳入進來的callable物件不為空並且執行緒狀態也是新建的
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    /**
                     * 原來我們總是說的call()方法原來是在run方法中執行的
                     * 然後call()返回一個泛型型別的返回值 ,這種通過實現介面的方法在我們平時中是很常見的吧.
                     */
                    result = c.call();
                    ran = true;
                   /**
                    * 該地方作者考慮的很清楚,在定義call方法的時候丟擲異常,然後這裡捕捉異常進行
                    * 處理,因為我們在call方法中寫程式碼難免會有異常問題的。
                    */
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                //如果call方法中不跑出異常的話,則通過set()方法將結果儲存起來
                //該set()方法其實也是Future介面定義的方法
                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方法,根據上面的程式碼我們其實也沒有什麼好看的,call()方法就是在run()方法中執行的。

protected void set(V v) {
    if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
        //最後將結果賦值給成員變數 outcome
        outcome = v;
        //同時更新state狀態為NORMAL,這是這種更新方式是直接根據記憶體地址來修改記憶體值的
        U.putOrderedInt(this, STATE, NORMAL); // final state
        finishCompletion();
    }
}

boolean compareAndSwapInt() 方法是Unsafe中的本地方法,主要作用是用於在多執行緒中併發的修改和讀取某個值,其主要原理:根據傳入的期望的資料跟記憶體中的資料進行對比,如果期望的資料跟記憶體中的資料相同的話,說明該變數的值沒有被其他的執行緒修改過,同時將我們需要更改的新資料替換記憶體中的資料,體會成功之後並且返回true,表示的是修改新資料成功了。相反如果有其他執行緒修改了記憶體中的則放棄更新新資料,並且返回true。它有三個引數:object 表示更改資料的物件;offset 表示物件上欄位的偏移地址;expectedValue 表示期望的資料,該資料的作用是用於跟記憶體中的資料進行比較,如果兩者不想等的話說明記憶體中的資料被其他執行緒修改過;newValue表示更新的值。最後如果更新某個記憶體地址的值成功的話,則返回true,否則返回false。這個也是我們平時用到的多執行緒併發的原理基石理論:CAS(Compare and Swap, 翻譯成比較並交換)。

private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            //根據尋找記憶體地址的方式來修改屬性在記憶體中的值,將該waiters物件置為null
            if (U.compareAndSwapObject(this, WAITERS, 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;
            }
        }
        //回撥Future介面的方法,標識中程式正常的結束了,我們需要重寫該方法.
        done();
        //特別需要注意的是需要將該介面變數置為空,防止出現因為引用問題導致內粗洩漏
        callable = null;        // to reduce footprint,
    }

當執行到該方法的時候也標識的程式正常的執行結束了,首先會將所有等待的執行緒全部喚醒,因為在執行FutureTask任務的時候呼叫get()方法是阻塞的,因為call()方法都還沒有執行完成,這個時候你是獲取不到任何結果的,所以會將當前呼叫get()方法的執行緒阻塞等待,直到呼叫finishCompletion()方法來解除執行緒阻塞,最後呼叫done()方法,這個時候我們就可以在該結束方法中執行我們想要的邏輯了;從程式碼中我們可以看出done()方法其實也還是執行在子執行緒的,所以我們並不可以在done()方法中更新UI的,還是需要Handler來傳送訊息的。

線上程都全部執行結束之後,我們就可以在done()方法通過呼叫get()方法來獲取最後執行的結果了,也就是剛剛在set()方法中看到的outcome的值。

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

我們在之前在呼叫set()方法時通過定址(根據記憶體地址的偏移量)的方式修改過了state的值為NORMAL了,所以NORMAL大於COMPLETING,最後直接呼叫report()方法,最後直接通過return x 來返回結果。這個也就是我們使用FutureTask的一個大概的流程。其實通過程式碼我們就能很容易的看出該類的大概的設計原理,同時還可以學到更多的其他技術。

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

我們通過呼叫Future介面的isDone()來判斷程式是否結束,可以直接根據state的狀態判斷是否是新建立的,該類的執行緒有7中不同的狀態,主要狀態切換成其中的一種我們就可以說程式結束了。

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;

public boolean isDone() {
    return state != NEW;
}

只要狀態值大於CANCELLED(4),也就是使用者主動呼叫cancel()方法,不管是主動中斷執行緒還是其他的方式都屬於取消的操作的。

public boolean isCancelled() {
    return state >= CANCELLED;
}

當程式裡面的FutureTask未執行完成的時候get()方法會一直阻塞呼叫該方法的執行緒,直到FutureTask裡面的任務執行才會解除阻塞。所以get()方法是一個阻塞式的去獲取結果的,從上面的get()方法的程式碼中我們可以得出當狀態還是NEW的時候,會呼叫awaitDone(false ,0)方法。

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // The code below is very delicate, to achieve these goals:
    // - call nanoTime exactly once for each call to park
    // - if nanos <= 0L, return promptly without allocation or nanoTime
    // - if nanos == Long.MIN_VALUE, don't underflow
    // - if nanos == Long.MAX_VALUE, and nanoTime is non-monotonic
    //   and we suffer a spurious wakeup, we will do no worse than
    //   to park-spin for a while
    long startTime = 0L;    // Special value 0L means not yet parked
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING)
            // We may have already promised (via isDone) that we are done
            // so never return empty-handed or throw InterruptedException
            Thread.yield();
        else if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        else if (q == null) {
            if (timed && nanos <= 0L)
                return s;
            q = new WaitNode();
        }
        else if (!queued)
            queued = U.compareAndSwapObject(this, WAITERS,
                                            q.next = waiters, q);
        else if (timed) {
            ........
        }
        else
            LockSupport.park(this);
    }
}

該方法有個無限迴圈知道狀態值大於COMPLETING才返回一個狀態值,我們線上程未執行完成的時候呼叫了get()方法,可以看到首先會建立一個WaitNode物件,然後通過Unsafe類來更新成員變數waiter的值為 q,然後再次迴圈最後會進入 LockSupport.park(this) 分支,該函式主要是獲取許可阻塞當前的執行緒,直到程式執行結束之後,呼叫LockSupport.unpark(this)來釋放阻塞。所以如果我們在主執行緒中直接呼叫get()方法來獲取結果的話則很有可能導致ANR,直到程式結束之後才會釋放阻塞的,正確的用法就是在done()方法裡面呼叫get()來獲取執行的結果的。關於LockSupport是一個非常重要的多執行緒併發的類,不懂的直接在百度上看看其解釋。

我們平時在使用AsyncTask的時候有一個cancel()方法來取消當前執行的任務,我們之前也說了AsyncTask的本質其實也是使用了FutureTask來實現的。其實它的cancel()方法也是呼叫FutureTask的取消方法的,下面看看取消的原理:

//如果返回值為true的話表示取消成功,否則為取消失敗了
public boolean cancel(boolean mayInterruptIfRunning) {
    //首先判斷當前的狀態是否是NEW,然後在通過Unsafe類去更新記憶體中state欄位的值為cancel。 
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {
        //如果以上的狀態值設定成功的話,則判斷是否設定中斷執行
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                //直接通過呼叫Thread的中斷方法來強制中斷當前執行的執行緒
                if (t != null)
                    t.interrupt();
            } finally { // final state
                //最後修改當前狀態state的值為 INTERRUPTED 為中斷
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        //最後解鎖所有被阻塞的執行緒
        finishCompletion();
    }
    return true;
}

我們在取消任務的時候可以設定強制中斷執行緒執行,只要呼叫cancel(true) 就行了,有時候我們呼叫cancel(false)並不能立刻的停止執行緒執行完成的,因為這個時候程式在run()方法中已經執行過了狀態(state)值判斷的話,這個時候就直接執行call()方法了,但是call() 方法也沒有執行完成,如果這個時候我們去取消的話, 因為我們知道取消的原理就是使用Unsafe類去修改記憶體中的state的值,但是這個時候設定已經來不急了。

public void run() {
        //第一次執行緒狀態判斷
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            //第二次執行緒判斷,如果我們的設定了一般的取消操作比該判斷滯後的話則是沒有什麼用的。
            if (c != null && state == NEW) {
                .....

                result = c.call();

                .....
            }
        } finally {
            ........
        }
    }

雖然我們呼叫了cancel(false)方法去取消任務的,但是很多的時候還是不能馬上終止任務執行,最後執行緒還是會繼續執行的,但是到了set()方法的時候,這裡會有一個狀態值的判斷的。之前我們已經介紹了執行緒併發的基石CAS,首先我們使用Unsafe類去比較state狀態值是否發生了變化,如果state的值被其他的執行緒修改了,則不會呼叫done()方法了。

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        ........
    }
}

總結

從上面我們對原始碼的分析的分析很明顯的知道了 Callable、Future、FutureTask三者之間的關係了,也很明白的知道的如何更好的使用它們了,其實FutureTask根本就沒有想象的這麼難,我看網上把FutureTask說的神乎其神,不會丟擲什麼異常,然後可以返回獲取結果等等各種各樣的解釋,由於沒有例子和程式碼,最後還是不知道講的是什麼。最後發現不過就是一個介面函式在run()方法中執行,我們只需要實現Callable介面並且重寫call()方法就可以了。不過通過看原始碼可以使我們學到很多的東西。

本次有兩個非常重要的東西很值得我們繼續去研究,由於本人理解水平有限,部落格裡面的內容寫的比較亂,有什麼疑問的大家一起討論和學習。

  • Unsafe 類的使用以及作用
  • LockSupport 作用以及使用
  • CAS (Compare and Swap, 比較並交換),該原理是所有執行緒併發的原理,有時間可以深入瞭解下

相關文章