FutureTask使用及源碼分析

最近工作中,遇到一個需求:300ms內請求到服務器返回值,否則取消請求。完成這個需求的時候,使用到了FutureTask。在這裏就記錄一下,並且研究一下其實現的原理。

文章順序:

  1. FutureTask的使用。
  2. 開發中可能出現的問題。
  3. 結合FutureTask的源碼分析問題。
  4. 精華部分

1. FutureTask的使用

在Java中,一般是通過繼承Thread類或者實現Runnable接口來創建多線程, Runnable接口不能返回結果,如果要獲取子線程的執行結果,一般都是在子線程執行結束之後,通過Handler將結果返回到調用線程,jdk1.5之後,Java提供了Callable接口來封裝子任務,Callable接口可以獲取返回結果。

Callable接口

下面可以看一下Callable接口的定義:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable接口很簡單,是一個泛型接口,就是定義了一個call()方法,與Runnable的run()方法相比,這個有返回值,泛型V就是要返回的結果類型,可以返回子任務的執行結果。

Future接口

Future接口表示異步計算的結果,通過Future接口提供的方法,可以很方便的查詢異步計算任務是否執行完成,獲取異步計算的結果,取消未執行的異步任務,或者中斷異步任務的執行,接口定義如下:

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}
  1. cancel(boolean mayInterruptIfRunning):取消子任務的執行,如果這個子任務已經執行結束,或者已經被取消,或者不能被取消,這個方法就會執行失敗並返回false;如果子任務還沒有開始執行,那麼子任務會被取消,不會再被執行;如果子任務已經開始執行了,但是還沒有執行結束,根據mayInterruptIfRunning的值,如果mayInterruptIfRunning = true,那麼會中斷執行任務的線程,然後返回true,如果參數爲false,會返回true,不會中斷執行任務的線程。這個方法在FutureTask的實現中有很多值得關注的地方,後面再細說。
    需要注意,這個方法執行結束,返回結果之後,再調用isDone()會返回true。
  2. isCancelled(),判斷任務是否被取消,如果任務執行結束(正常執行結束和發生異常結束,都算執行結束)前被取消,也就是調用了cancel()方法,並且cancel()返回true,則該方法返回true,否則返回false.
  3. isDone():判斷任務是否執行結束,正常執行結束,或者發生異常結束,或者被取消,都屬於結束,該方法都會返回true.
  4. V get():獲取結果,如果這個計算任務還沒有執行結束,該調用線程會進入阻塞狀態。如果計算任務已經被取消,調用get()會拋出CancellationException,如果計算過程中拋出異常,該方法會拋出ExecutionException,如果當前線程在阻塞等待的時候被中斷了,該方法會拋出InterruptedException。
  5. V get(long timeout, TimeUnit unit):帶超時限制的get(),等待超時之後,該方法會拋出TimeoutException。

FutureTask

FutureTask可以像Runnable一樣,封裝異步任務,然後提交給Thread或線程池執行,然後獲取任務執行結果。原因在於FutureTask實現了RunnableFuture接口,RunnableFuture是什麼呢,其實就是Runnable和Future的結合,它繼承自Runnable和Future。繼承關係如下:

public class FutureTask<V> implements RunnableFuture<V> {
    ...
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

FutureTask使用

1. FutureTask + Thread :上面介紹過,FutureTask有Runnable接口和Callable接口的特徵,可以被Thread執行。

//step1:封裝一個計算任務,實現Callable接口   
class Task implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        try {
            for (int i = 0; i < 10; i++) {
                Log.d(TAG, "task..." + Thread.currentThread().getName() + "...i = " + i);
                //模擬耗時操作
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Log.e(TAG, " is interrupted when calculating, will stop...");
            return false; // 注意這裏如果不return的話,線程還會繼續執行,所以任務超時後在這裏處理結果然後返回
        }
        return true;
    }
}
//step2:創建計算任務,作爲參數,傳入FutureTask
Task task = new Task();
FutureTask futureTask = new FutureTask(task);//step3:將FutureTask提交給Thread執行
Thread thread1 = new Thread(futureTask);
thread1.setName("task thread 1");
thread1.start();
//step4:獲取執行結果,由於get()方法可能會阻塞當前調用線程,如果子任務執行時間不確定,最好在子線程中獲取執行結果try {
    // boolean result = (boolean) futureTask.get();
    boolean result = (boolean) futureTask.get(5, TimeUnit.SECONDS);
    Log.d(TAG, "result:" + result);
} catch (InterruptedException e) {
    Log.e(TAG, "守護線程阻塞被打斷...");
    e.printStackTrace();
} catch (ExecutionException e) {
    Log.e(TAG, "執行任務時出錯...");
    e.printStackTrace();
} catch (TimeoutException e) {
    Log.e(TAG, "執行超時...");
    futureTask.cancel(true);
    e.printStackTrace();
} catch (CancellationException e) {
    //如果線程已經cancel了,再執行get操作會拋出這個異常
    Log.e(TAG, "future已經cancel了...");
    e.printStackTrace();
}

2. Future + ExecutorService

//step1 封裝一個計算任務,實現Callable接口 如上
//step2:創建計算任務 如上
Task task = new Task();
//step3:創建線程池,將Callable類型的task提交給線程池執行,通過Future獲取子任務的執行結果
ExecutorService executorService = Executors.newCachedThreadPool();
final Future<Boolean> future = executorService.submit(task);
//step4:通過future獲取執行結果
boolean result = (boolean) future.get();

3. FutureTask + ExecutorService

//step1 封裝一個計算任務,實現Callable接口 如上
//step2:創建計算任務 如上
//step3:將FutureTask提交給線程池執行
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(futureTask);
//step4:通過future獲取執行結果
boolean result = (boolean) futureTask.get();

2. 開發可能出現的問題。

FutureTask使用還是比較簡單的,FutureTask與Runnable,最大的區別有兩個,一個是可以獲取執行結果,另一個是可以取消,使用方法可以參考以上步驟,不過使用FutureTask可能出現以下兩個問題:

  1. 有的情況下,使用 futuretask.cancel(true)方法並不能真正的結束子任務執行。
  2. FutureTask的get(long timeout, TimeUnit unit)方法,是等待timeout時間後,獲取子線程的執行結果,但是如果子任務執行結束了,但是超時時間還沒有到,這個方法也會返回結果。

3. 結合FutureTask的源碼分析問題。

下面,結合FutureTask的源碼,分析一下以上兩個問題。

成員變量

先看一下FutureTask內部比較值得關注的幾個成員變量。

public class FutureTask<V> implements RunnableFuture<V> {
    
    private volatile int state;
    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes
    /** The thread running the callable; CASed during run() */
    private volatile Thread runner;
    /** Treiber stack of waiting threads */
    private volatile WaitNode waiters;
    ...
}
  1. private volatile int state,state用來標識當前任務的運行狀態。FutureTask的所有方法都是圍繞這個狀態進行的,需要注意,這個值用volatile(易變的)來標記,如果有多個子線程在執行FutureTask,那麼它們看到的都會是同一個state,有如下幾個值:
     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:表示任務執行結束(正常執行結束,或者發生異常結束),但是還沒有將結果保存到outcome中。是一箇中間狀態。
    NORMAL:表示任務正常執行結束,並且已經把執行結果保存到outcome字段中。是一個最終狀態。
    EXCEPTIONAL:表示任務發生異常結束,異常信息已經保存到outcome中,這是一個最終狀態。
    CANCELLED:任務在新建之後,執行結束之前被取消了,但是不要求中斷正在執行的線程,也就是調用了cancel(false),任務就是CANCELLED狀態,這時任務狀態變化是NEW -> CANCELLED
    INTERRUPTING:任務在新建之後,執行結束之前被取消了,並要求中斷線程的執行,也就是調用了cancel(true),這時任務狀態就是INTERRUPTING。這是一箇中間狀態。
    INTERRUPTED:調用cancel(true)取消異步任務,會調用interrupt()中斷線程的執行,然後狀態會從INTERRUPTING變到INTERRUPTED。

    狀態變化有如下4種情況:
    NEW -> COMPLETING -> NORMAL --------------------------------------- 正常執行結束的流程
    NEW -> COMPLETING -> EXCEPTIONAL ---------------------執行過程中出現異常的流程
    NEW -> CANCELLED -------------------------------------------被取消,即調用了cancel(false)
    NEW -> INTERRUPTING -> INTERRUPTED -------------被中斷,即調用了cancel(true)

  2. private Callable<V> callable,一個Callable類型的變量,封裝了計算任務,可獲取計算結果。從上面的用法中可以看到,FutureTask的構造函數中,我們傳入的就是實現了Callable的接口的計算任務。

  3.  private Object outcome,Object類型的變量outcome,用來保存計算任務的返回結果,或者執行過程中拋出的異常。
  4. private volatile Thread runner,指向當前在運行Callable任務的線程,runner在FutureTask中的賦值變化很值得關注,後面源碼會詳細介紹這個。
  5. private volatile WaitNode waiters,WaitNode是FutureTask的內部類,表示一個阻塞隊列,如果任務還沒有執行結束,那麼調用get()獲取結果的線程會阻塞,在這個阻塞隊列中排隊等待。

成員函數

下面從構造函數開始,看一下FutureTask的源碼。

1. 構造函數

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

FutureTask的第一個構造函數,參數是Callable類型的變量。將傳入的參數賦值給this.callable,然後設置state狀態爲NEW,表示這是新任務。

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

FutureTask還有一個構造函數,接收Runnable類型的參數,通過Executors.callable(runnable, result)將傳入的Runnable和result轉換成Callable類型。使用該構造方法,可以定製返回結果。

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}

可以看一下Executors.callable(runnable, result)方法,這裏通過適配器模式進行適配,創建一個RunnableAdapter適配器。

private static final class RunnableAdapter<T> implements Callable<T> {
    private final Runnable task;
    private final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

RunnableAdapter是Executors的內部類,實現也比較簡單,實現了適配對象Callable接口,在call()方法中執行Runnable的run(),然後返回result。

2. 任務被執行——run()

FutureTask封裝了計算任務,無論是提交給Thread執行,或者線程池執行,調用的都是FutureTask的run()。

public void run() {
    //1.判斷狀態是否是NEW,不是NEW,說明任務已經被其他線程執行,甚至執行結束,或者被取消了,直接返回
    //2.調用CAS方法,判斷RUNNER爲null的話,就將當前線程保存到RUNNER中,設置RUNNER失敗,就直接返回
    if (state != NEW ||
            !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                //3.執行Callable任務,結果保存到result中
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                //3.1 如果執行任務過程中發生異常,將調用setException()設置異常
                result = null;
                ran = false;
                setException(ex);
            }
            //3.2 任務正常執行結束調用set(result)保存結果
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        //4. 任務執行結束,runner設置爲null,表示當前沒有線程在執行這個任務了
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        //5. 讀取狀態,判斷是否在執行的過程中,被中斷了,如果被中斷,處理中斷
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}
  1. 首先,判斷state的值是不是NEW,如果不是NEW,說明線程已經被執行了,可能已經執行結束,或者被取消了,直接返回。
  2. 這裏其實是調用了Unsafe的CAS方法,讀取並設置runner的值,將當前線程保存到runner中,表示當前正在執行任務的線程。可以看到,這裏設置的其實是RUNNER,和前面介紹的Thread類型的runner變量不一樣的,那爲什麼還說設置的是runner的值?RUNNER在FutureTask中定義如下:
    ​
    private static final long RUNNER;//RUNNER是一個long類型的變量,指向runner字段的偏移地址,相當於指針
    RUNNER = U.objectFieldOffset(FutureTask.class.getDeclaredField("runner"));
    
    ​

    關於Unsafe的CAS方法,簡單介紹一下,它提供了一種對runner進行原子操作的方法,原子操作,意味着,這個操作不會被打斷。runner被volatile字段修飾,只能保證,當多個子線程在執行FutureTask的時候,它們讀取到的runner的值是同一個,但是不能保證原子操作,所以很容易讀到髒數據(舉個例子:線程A準備對runner進行讀和寫操作,讀取到runner的值爲null,這是,cpu切換執行線程B,線程B讀取到runner的值也是null,然後又切換到線程A執行,線程A對runner賦值thread-A,此時runner的值已經不再是null,線程B讀取到的runner=null就是髒數據),用Unsafe的CAS方法,來對runner進行讀寫,就能保證原子操作。多個線程訪問run()方法時,會在這裏同步。

  3. 讀取callable變量,執行call(),並獲取執行結果。
    如果執行call()的過程中發生異常,就調用setException()設置異常,setException()定義如下:

    protected void setException(Throwable t) {
        if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
            outcome = t;
            U.putOrderedInt(this, STATE, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }
    //a. 調用Unsafe的CAS方法,state從NEW --> COMPLETING,這裏的STATE和上面的RUNNER定義類似,指向state字段的偏移地址。
    //b. 將異常信息保存到outcome字段,state變成EXCEPTIONAL。
    //c. 調用finishCompletion()。
    //NEW --> COMPLETING --> EXCEPTIONAL。

     

  4. 任務執行結束,runner設置爲null,表示當前沒有線程在執行這個任務了。
  5. 讀取state狀態,判斷是否在執行的過程中被中斷了,如果被中斷,處理中斷,看一下這個中斷處理:
    private void handlePossibleCancellationInterrupt(int s) {
        // It is possible for our interrupter to stall before getting a
        // chance to interrupt us.  Let's spin-wait patiently.
        if (s == INTERRUPTING)
            while (state == INTERRUPTING)
                Thread.yield(); // wait out pending interrupt
    }

    如果狀態是INTERRUPTING,表示正在被中斷,這時就讓出線程的執行權,給其他線程來執行。

3. 獲取任務的執行結果——get()

一般情況下,執行任務的線程和獲取結果的線程不會是同一個,當我們在主線程或者其他線程中,獲取計算任務的結果時,就會調用get方法,如果這時計算任務還沒有執行完成,調用get()的線程就會阻塞等待。get()實現如下:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
  1. 讀取任務的執行狀態 state ,如果 state <= COMPLETING,說明線程還沒有執行完(run()中可以看到,只有任務執行結束,或者發生異常的時候,state纔會被設置成COMPLETING)。
  2. 調用awaitDone(false, 0L),進入阻塞狀態。看一下awaitDone(false, 0L)的實現:
    private int awaitDone(boolean timed, long nanos)
            throws InterruptedException {
        long startTime = 0L;    // Special value 0L means not yet parked
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            //1. 讀取狀態
            //1.1 如果s > COMPLETING,表示任務已經執行結束,或者發生異常結束了,就不會阻塞,直接返回
            int s = state;
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //1.2 如果s == COMPLETING,表示任務結束(正常/異常),但是結果還沒有保存到outcome字段,當前線程讓出執行權,給其他線程先執行
            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();
            //2. 如果調用get()的線程被中斷了,就從等待的線程棧中移除這個等待節點,然後拋出中斷異常
            else if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }
            //3. 如果等待節點q=null,就創建一個等待節點
            else if (q == null) {
                if (timed && nanos <= 0L)
                    return s;
                q = new WaitNode();
            }
            //4. 如果這個等待節點還沒有加入等待隊列,就加入隊列頭
            else if (!queued)
                queued = U.compareAndSwapObject(this, WAITERS,
                        q.next = waiters, q);
            //5. 如果設置了超時等待時間
            else if (timed) {
                //5.1 設置startTime,用於計算超時時間,如果超時時間到了,就等待隊列中移除當前節點
                final long parkNanos;
                if (startTime == 0L) { // first time
                    startTime = System.nanoTime();
                    if (startTime == 0L)
                        startTime = 1L;
                    parkNanos = nanos;
                } else {
                    long elapsed = System.nanoTime() - startTime;
                    if (elapsed >= nanos) {
                        removeWaiter(q);
                        return state;
                    }
                    parkNanos = nanos - elapsed;
                }
                // nanoTime may be slow; recheck before parking
                //5.2 如果超時時間還沒有到,而且任務還沒有結束,就阻塞特定時間
                if (state < COMPLETING)
                    LockSupport.parkNanos(this, parkNanos);
            }
            //6. 阻塞,等待喚醒
            else
                LockSupport.park(this);
        }
    }

    這裏主要有幾個步驟:
    a. 讀取state,如果s > COMPLETING,表示任務已經執行結束,或者發生異常結束了,此時,調用get()的線程就不會阻塞;如果s == COMPLETING,表示任務結束(正常/異常),但是結果還沒有保存到outcome字段,當前線程讓出執行權,給其他線程先執行。
    b. 判斷Thread.interrupted(),如果調用get()的線程被中斷了,就從等待的線程棧(其實就是一個WaitNode節點隊列或者說是棧)中移除這個等待節點,然後拋出中斷異常。
    c. 判斷q == null,如果等待節點q爲null,就創建等待節點,這個節點後面會被插入阻塞隊列。
    d. 判斷queued,這裏是將c中創建節點q加入隊列頭。使用Unsafe的CAS方法,對waiters進行賦值,waiters也是一個WaitNode節點,相當於隊列頭,或者理解爲隊列的頭指針。通過WaitNode可以遍歷整個阻塞隊列。
    e. 之後,判斷timed,這是從get()傳入的值,表示是否設置了超時時間。設置超時時間之後,調用get()的線程最多阻塞nanos,就會從阻塞狀態醒過來。如果沒有設置超時時間,就直接進入阻塞狀態,等待被其他線程喚醒。

    awaitDone()方法內部有一個無限循環,看似有很多判斷,比較難理解,其實這個循環最多循環3次。
    假設Thread A執行了get()獲取計算任務執行結果,但是子任務還沒有執行完,而且Thread A沒有被中斷,它會進行以下步驟。
    step1:Thread A執行了awaitDone(),1,2兩次判斷都不成立,Thread A判斷q=null,會創建一個WaitNode節點q,然後進入第二次循環。
    step2:第二次循環,判斷4不成立,此時將step1創建的節點q加入隊列頭。
    step3:第三次循環,判斷是否設置了超時時間,如果設置了超時時間,就阻塞特定時間,否則,一直阻塞,等待被其他線程喚醒。

  3. awaitDone()返回,最後調用report(int s),這個後面再介紹。
  4. 取消任務——cancel(boolean mayInterruptIfRunning)

    通常調用cancel()的線程和執行子任務的線程不會是同一個。當FutureTaskcancel(boolean mayInterruptIfRunning)方法被調用時,如果子任務還沒有執行,那麼這個任務就不會執行了,如果子任務已經執行,且mayInterruptIfRunning=true,那麼執行子任務的線程會被中斷(注意:這裏說的是線程被中斷,不是任務被取消),下面看一下這個方法的實現:

    public boolean cancel(boolean mayInterruptIfRunning) {
        //1.判斷state是否爲NEW,如果不是NEW,說明任務已經結束或者被取消了,該方法會執行返回false
        //state=NEW時,判斷mayInterruptIfRunning,如果mayInterruptIfRunning=true,說明要中斷任務的執行,NEW->INTERRUPTING
        //如果mayInterruptIfRunning=false,不需要中斷,狀態改爲CANCELLED
        if (!(state == NEW &&
                U.compareAndSwapInt(this, STATE, NEW,
                        mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    //2.讀取當前正在執行子任務的線程runner,調用t.interrupt(),中斷線程執行
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    //3.修改狀態爲INTERRUPTED
                    U.putOrderedInt(this, STATE, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

    cancel()分析:

    1. 判斷state,保證state = NEW才能繼續cancel()的後續操作。state=NEW且mayInterruptIfRunning=true,說明要中斷任務的執行,此時,NEW->INTERRUPTING。然後讀取當前執行任務的線程runner,調用t.interrupt(),中斷線程執行,NEW->INTERRUPTING->INTERRUPTED,最後調用finishCompletion()。
    2. 如果NEW->INTERRUPTING,那麼cancel()方法,只是修改了狀態,NEW->CANCELLED,然後直接調用finishCompletion()。 
    3. 所以cancel(true)方法,只是調用t.interrupt(),此時,如果t因爲sleep(),wait()等方法進入阻塞狀態,那麼阻塞的地方會拋出InterruptedException;如果線程正常運行,需要結合Thread的interrupted()方法進行判斷,才能結束,否則,cancel(true)不能結束正在執行的任務。
      這也就可以解釋,有的情況下,使用 futuretask.cancel(true)方法並不能真正的結束子任務執行。
  5.  子線程返回結果前的最後一步——finishCompletion()

    前面多次出現過這個方法,set(V v)(保存執行結果,設置狀態爲NORMAL),setException(Throwable t)(保存結果,設置狀態爲EXCEPTIONAL)和cancel(boolean mayInterruptIfRunning)(設置狀態爲CANCELLED/INTERRUPTED),該方法在state變成最終態之後,會被調用。

    private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = 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;
            }
        }
    
        done();
    
        callable = null;        // to reduce footprint
    }

    finishCompletion()主要做了三件事情:

    1. 遍歷waiters等待隊列,調用LockSupport.unpark(t)喚醒等待返回結果的線程,釋放資源。
    2. 調用done(),這個方法什麼都沒有做,不過子類可以實現這個方法,做一些額外的操作。
    3. 設置callable爲null,callable是FutureTask封裝的任務,任務執行完,釋放資源。

這裏可以解答上面的第二個問題了。FutureTask的get(long timeout, TimeUnit unit)方法,表示阻塞timeout時間後,獲取子線程的執行結果,但是如果子任務執行結束了,但是超時時間還沒有到,這個方法也會返回結果。因爲任務執行完之後,會遍歷阻塞隊列,喚醒阻塞的線程。LockSupport.unpark(t)執行之後,阻塞的線程會從LockSupport.park(this)/LockSupport.parkNanos(this, parkNanos)醒來,然後會繼續進入awaitDone(boolean timed, long nanos)的while循環,此時,state >= COMPLETING,然後從awaitDone()返回。此時,get()/get(long timeout, TimeUnit unit)會繼續執行,return report(s),上面介紹get()的時候沒介紹的方法。看一下report(int 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);
}

其實就是讀取outcome,將state映射到最後返回的結果中,s == NORMAL說明任務正常結束,返回正常結果,s >= CANCELLED,就拋出CancellationException。

這裏補充一下:

LockSupport.parkNanos與Thread.Sleep的確具有相似的語義。二者的最終都是根據條件變量超時等待函數。LockSupport.parkNanos():爲了線程調度,在指定的時限前禁用當前線程,除非許可可用。是可以在阻塞時間沒有到的時候,通過unpark來喚醒,提前跳出阻塞。
Sleep()是單純的在指定時間內讓當前正在執行的線程暫停執行,但不會釋放“鎖標誌”,使當前線程進入阻塞狀態,在指定時間內不會執行。

6.其他方法

FutureTask的還有兩個方法isCancelled()和isDone(),其實就是判斷state,沒有過多的步驟。

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

4. 精華部分

LockSupport中的park() 和 unpark() 的作用分別是阻塞線程和解除阻塞線程,其使用方式:簡單,靈活

簡單

這裏先通過對比來介紹LockSupport。在沒有LockSupport之前,線程的掛起和喚醒咱們都是通過Object的wait和notify/notifyAll方法實現。

寫一段例子代碼,線程A執行一段業務邏輯後調用wait阻塞住自己。主線程調用notify方法喚醒線程A,線程A然後打印自己執行的結果。

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    obj.wait();
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        Thread.sleep(1000);
        obj.notify();
    }
}

執行這段代碼,不難發現這個錯誤:

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)

原因很簡單,wait和notify/notifyAll方法只能在同步代碼塊裏用。所以將代碼修改爲如下就可正常運行了:

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}

那如果換成LockSupport呢?簡單得很,看代碼:

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                LockSupport.park();
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        Thread.sleep(1000);
        LockSupport.unpark(A);
    }
}
直接調用就可以,沒有說非得在同步代碼塊裏才能用。

靈活

先看一個例子:

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        //Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}

多運行幾次上邊的代碼,有的時候能夠正常打印結果並退出程序,但有的時候線程無法打印結果阻塞住了。原因就在於:主線程調用完notify後,線程A才進入wait方法,導致線程A一直阻塞住。由於線程A不是後臺線程,所以整個程序無法退出。

那如果換做LockSupport呢?LockSupport就支持主線程先調用unpark後,線程A再調用park而不被阻塞嗎?是的,沒錯。代碼如下:

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                LockSupport.park();
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        //Thread.sleep(1000);
        LockSupport.unpark(A);
    }
}

不管你執行多少次,這段代碼都能正常打印結果並退出。這就是LockSupport最大的靈活所在。

總結一下,LockSupport比Object的wait/notify有三大優勢

  1. LockSupport不需要在同步代碼塊裏 。所以線程間也不需要維護一個共享的同步對象了,實現了線程間的解耦。
  2. unpark函數可以先於park調用,因爲這個特點,我們可以不用擔心掛起和恢復時序問題。。
  3. 結合1和2,得到的第三點優勢:使用比較不容易出錯,對線程阻塞與喚醒時可以優先選擇這種方式而不是object的wait和notify的方式。

 

LockSupport的實現

學習要知其然,還要知其所以然。下面看看LockSupport的實現:

進入LockSupport的park方法,可以發現它是調用了Unsafe的park方法,這是一個本地native方法,只能通過openjdk的源碼看看其本地實現了。 

它調用了線程的Parker類型對象的park方法,如下是Parker類的定義:

類中定義了一個int類型的_counter變量,咱們上文中講靈活性的那一節說,可以先執行unpark後執行park,就是通過這個變量實現,看park方法的實現代碼(由於方法比較長就不整體截圖了):

park方法會調用Atomic::xchg方法,這個方法會原子性的將_counter賦值爲0,並返回賦值前的值。如果調用park方法前,_counter大於0,則說明之前調用過unpark方法,所以park方法直接返回。

接着往下看:

實際上Parker類用Posix的mutex,condition來實現的阻塞喚醒。如果對mutex和condition不熟,可以簡單理解爲mutex就是Java裏的synchronized,condition就是Object裏的wait/notify操作。

park方法裏調用pthread_mutex_trylock方法,就相當於Java線程進入Java的同步代碼塊,然後再次判斷_counter是否大於零,如果大於零則將_counter設置爲零。最後調用pthread_mutex_unlock解鎖,相當於Java執行完退出同步代碼塊。如果_counter不大於零,則繼續往下執行pthread_cond_wait方法,實現當前線程的阻塞。

 

最後再看看unpark方法的實現吧,這塊就簡單多了,直接上代碼:

圖中的1和4就相當於Java的進入synchronized和退出synchronized的加鎖解鎖操作,代碼2將_counter設置爲1,同時判斷先前_counter的值是否小於1,即這段代碼:if(s<1)。如果不小於1,則就不會有線程被park,所以方法直接執行完畢,否則就會執行代碼3,來喚醒被阻塞的線程。

 

通過閱讀LockSupport的本地實現,我們不難發現這麼個問題:多次調用unpark方法和調用一次unpark方法效果一樣,因爲都是直接將_counter賦值爲1,而不是加1。簡單說就是:線程A連續調用兩次LockSupport.unpark(B)方法喚醒線程B,然後線程B調用兩次LockSupport.park()方法, 線程B依舊會被阻塞。因爲兩次unpark調用效果跟一次調用一樣,只能讓線程B的第一次調用park方法不被阻塞,第二次調用依舊會阻塞。

總結

到此FutureTask分析完畢,其中感受最深的是Unsafe的用法,對於多線程共享的對象,採用volatile + Unsafe的方法,代替鎖操作,進行同步;其次,是LockSupportpark(Object blocker)unpark(Thread thread)的使用

  1. park(Object blocker):線程進入阻塞狀態,告訴線程調度,當前線程不可用,直到線程再次獲取permit(允許);如果在調用park(Object blocker)之前,線程已經獲得了permit(比如說,已經調用了unpark(t)),那麼該方法會返回。
  2. unpark(Thread thread):使得傳入的線程再次獲得permit.這裏的permit可以理解爲一個信號量,多次調用和一次調用的效果一致。

參考:

1.https://www.jianshu.com/p/55221d045f39

2.https://www.cnblogs.com/qingquanzi/p/8228422.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章