【Java併發編程】FutureTask源碼解讀

最近在學習netty,其中講到了異步回調,而Netty中的異步回調繼承並擴展了JdK中FutureTask相關的API,所以索性又把FutureTask源碼看了一遍,看完就覺得兩個字:🐂🍺!於是決定寫篇文章梳理梳理。

最近要做的東西太多了,嘮嗑概念啥的不多講了,直接開撕源碼吧!

一.  FutureTask簡介

 

我們都知道,Java中生成線程兩種最常見的方式是繼承Thread,和實現Runnable接口。而Thread其實也是實現了Runnable接口,因此這兩種啓動線程方式最終執行的都是重寫了Runnable接口裏面的run()方法,但是我們知道,run方法的返回值是void,所以我們通過這兩種方式無法獲取線程的執行結果。因此,java提供了FutureTask類和Callable接口來滿足我們對線程執行結果的需要,下圖是FutureTask的UML圖:

 

我們可以看到,FutureTask類實現了RunnableFuture接口,而這個接口實現了Runnable接口和Future接口。

實現了Runnable接口意味着FutureTask也可以傳給Thread來啓動線程,但你可能會有疑問?這FutureTask不還是繼承了Runnable接口,重寫了run方法嗎?嗯嗯,確實沒錯,但是你們看看UML圖中的FutureTask的兩個構造方法,其中一個傳入的是啥?Callable,而Clllable接口中call方法可以有返回值的,而FutureTask中實現的run方法最終調用的是Callable實例的call方法(另一個構造方法傳入的是Runnable,但最終還是通過適配模式將其轉變爲了Callable)。

好了,那Future接口又是幹啥的呢?它其實就是定義了對併發任務的執行及獲取其結果的一些操作方法,FutureTask對這些方法進行了實現,現在我們就好好來看看FutureTask的源碼吧!

 

二.  源碼解析

 

1.  屬性

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; //任務已中斷


private Callable<V> callable; //被提交的任務
private Object outcome; //任務完成後返回的結果或是異常拋出的錯誤
private volatile Thread runner; //執行任務的線程
private volatile WaitNode waiters; //等待的線程,是單向鏈表結構

 

state表示任務的執行狀態,狀態一共有上面的6種,而狀態的流轉過程一共有下面四種情況:

  1. NEW -> COMPLETING -> NORMAL :任務正常執行並返回

  2. NEW -> COMPLETING -> EXCEPTIONAL :執行中出現異常

  3. NEW -> CANCELLED :任務執行過程中被取消,並且不響應中斷

  4. NEW -> INTERRUPTING -> INTERRUPTED :任務執行過程中被取消,並且響應中斷

需要注意的是,只要state不爲NEW,就說明任務已經執行完了(等看後面的代碼就清楚了)。

waiters表示所有等待任務執行完畢的線程的集合,我們看下它的結構:

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

這是一個典型的單向鏈表結構,但是這個單向鏈表在FutureTask中是當做棧使用的,且是一個無鎖併發棧(Treiber Stack),這個棧的出棧與入棧是使用CAS來完成的,所以是線程安全的。

使用線程安全的棧是因爲在同一時刻,可能有多個線程在獲取執行任務(對任務進行操作,如get,cancel等),如果任務還在執行中,就會將此線程包裝成WaitNode放入棧頂,因此需要保證線程安全。出棧同理。waiters就是永遠指向棧頂的。

我們需要區別Treiber Stack中的線程與Runner屬性,runner屬性是指的執行任務的線程,即執行run方法的線程,而reiber Stack中存的是獲取任務狀態或結果的線程。

 

2.  方法

方法有許多,我們主要看構造方法、run方法,get方法和cancel方法:

 

2.1  構造方法

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

都是初始化屬性callable和state,需要注意的是,如果構造函數傳入的是Callable對象,則需要通過Executors將其適配成Callable對象。

 

2.2  run方法

public void run() {
    //如果狀態不爲NEW 或者 使用CAS操作將runner屬性設置位當前線程操作失敗的話 則直接返回
    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方法賦給outcome屬性
                setException(ex);
            }
            if (ran)
                // 將獲取的結果通過set方法賦給outcome屬性
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        // 防止其他線程將state更改,自旋判斷
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}


首先,我們會判斷state狀態是否爲NEW,並通過CAS操作將runner置爲本線程(runner此時必須爲null,如果不爲null,則說明此時有線程在調用),可以看到,runner是在運行時被初始化的。

接着就調用Callable對象的call方法來執行方法,如果執行成功,則調用set方法,否則調用setException方法。

接着我們就來看下set方法和setException方法:

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}
protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

 

set方法中,我們先將state屬性從NEW變爲COMPLETING,然後將結果賦給屬性outcome,然後再將屬性置爲NORMAL,最後執行finishCompletion()方法。

我們可以看到,當任務執行完成後,我們纔將state從NEW變爲COMPLETING,然後賦值完outcome後,又馬上變爲NORMAL,因此得出兩點:

  1. 所以state只要不是NEW,就表明任務已經完成了

  2. COMPLETING只是一個很短暫的中間狀態

setException方法和set方法大同小異,狀態變化不同而已。

我們再看下finishCompletion()方法,此時,任務都執行完了,因此這個方法和run方法finally塊裏面的代碼都是進行善後處理的。finishCompletion()是對屬性waiters進行善後(waiters置null並喚醒棧中線程),而finally塊裏面是對屬性runner和states進行善後,我們先說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
}

 

for循環是判斷Treiber棧的棧頂節點是否爲null,不爲null就繼續循環,而裏面的if條件則是將waiters屬性的值置爲null,如果不成功,則繼續跳到外層for循環,直到waiters爲null(所以這個for循環相當於一個自旋操作,目的是爲了確保waiters爲null)

waiters爲null後,我們將進入裏面的for循環來遍歷整個Treiber棧,將棧裏面的線程通過LockSupport.unpart方法一一喚醒,最後執行done方法(是個空方法,提供給子類覆寫來執行結束前的額外操作),將callable清理。

 

最後我們跳回到run方法看下finally裏面的程序:

finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
}

 

我們回想下set方法和setException方法,裏面已經把status狀態轉換成COMPLETING或EXCEPTIONAL了,這裏爲什麼還要判斷狀態是否>=INTERRUPTING,因爲多線程環境下,當前線程在執行run方法時,可能另一個線程執行了cancel方法,取消了任務的執行,因此將stats的值改了,所以這也是爲什麼在set或setException方法中,改變COMPLETING狀態時爲什麼使用了putOrderedInt直接更改status,而不是用compareAndSwapInt比較後再更改,因爲此時我們根本不確定原值是COMPLETING還是INTERUPING,可能此時COMPLETING已經被另一個線程更改了。

這裏需要特別注意,我們FutureTask中會涉及兩種線程,第一種是執行任務的線程,這種一般只有一個,而獲取結果的線程則會有多個。

handlePossibleCancellationInterrupt方法裏面相當於一個自旋,直到當status不爲INTERUPING時就完了。

總結下run方法,一共完成了下面幾件事:

  1. runner初始化

  2. 調用callable對象的call方法執行任務

  3. 任務結束後將state置爲中間態COMPLETING,並任務結果賦值給outcome

  4. 將state置爲終止態NORMAL或EXCEPTIONAL

  5. 喚醒Treiber棧中的所有線程

  6. 將runner,callable置爲null

  7. 驗證states是否爲終止態

 

2.3  get方法

get分爲無參和有參,我們看下無參的get方法:

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

如果state不是屬於最終狀態,則會執行awaitDone的方法,awatiDone方法裏面完成了獲取結果,響應中斷,掛起線程等功能。

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

初始化變量後,我們進入for循環,如果此時任務還未完成,則會進入到下面if分支:

else if (q == null)
    q = new WaitNode();
else if (!queued)
    queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                         q.next = waiters, q);

首先在第一個if分支生成一個WaitNode節點,然後在第二個分支將此節點放入棧首。因爲調用的是無參構造方法,所以傳入的timed==false,則又返回到for開始處,假設此時state的狀態變爲了中間態COMPLIETING,則會執行下面分支將線程掛起:

else if (s == COMPLETING) // cannot time out yet
    Thread.yield();

如果state爲終止態,則執行下面分支,q不爲null時則將其thread屬性置爲null,然後返回此時的狀態states:

if (s > COMPLETING) {
     if (q != null)
         q.thread = null;
     return s;
 }

當檢測到線程中斷時,則執行下面分支:

if (Thread.interrupted()) {
      removeWaiter(q);
      throw new InterruptedException();
 }

我們看下removeWaiter方法:

private void removeWaiter(WaitNode node) {
    if (node != null) {
        node.thread = null;
        retry:
        for (;;) {          // restart on removeWaiter race
            for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                s = q.next;
                if (q.thread != null)
                    pred = q;
                else if (pred != null) {
                    pred.next = s;
                    if (pred.thread == null) // check for race
                        continue retry;
                }
                else if (!UNSAFE.compareAndSwapObject(this, waitersOffset, q, s))
                    continue retry;
            }
            break;
        }
    }
}

 

我們先將出棧的node的thread屬性設置爲null,爲啥要這樣做,是因爲我們此時不知道此WaitNode是否在棧頂,所以我們需要在後面的for循環中遍歷棧找到此WaitNode位置並移除,而屬性thead爲null就是我們遍歷過程中定位此WaitNode的依據。

如果node在棧頂,則for循環中直接執行最後一個else if ,將棧頂節點的下一個節點變成棧頂節點。需要注意的是,不管此CAS操作是否成功,都需要跳回到for循環外的retry位置,然後執行for循環,遍歷完棧中的所有節點。

假如node不在棧頂,則最終會執行第一個else if,將出棧節點的前一個節點的next指向出棧節點的後一個節點(隊列的刪除操作)。可是爲什麼後面還有一個if判斷呢?因爲removeWaiter沒有加鎖,如果多個線程同時執行,前面一個節點此時被另一個線程將標記爲要拿出去棧的節點(因爲thred和next都是volatile修飾,因此它們的狀態具有可見性),則此時我們需要回到for循環外,再從頭遍歷棧,刪除此節點。所以removeWaiter方法不僅刪除傳入的節點,可能還會刪除在其他線程中標記爲需要刪除的節點,這樣就提升了效率。

我們最後再回到awaitDone方法,如果上面條件都不滿足,我們就執行最後一個分支,並執行LockSupport.park(this),將自己掛起,當任務執行完或調用取消操作時,會調用我們前面講的finishCompletion方法將所有掛起的線程喚醒,當然,如果有中斷,該線程也會被喚醒。

 

2.4  cancel方法

在講解cancel方法之前,我們先看下Future接口中對cancel方法的描述:

嘗試取消執行任務。當我們嘗試對某任務進行取消操作,如果此任務處於已經完成、已被取消過、或其他原因不能被取消這三種情況的一種,則此次取消操作失敗。如果成功,並且在調用cancel時此任務尚未啓動,則該任務永遠不會運行。如果任務已經啓動,而調用取消操作的線程則根據mayInterruptIfRunning參數來決定是否中斷被取消操作的線程。此方法返回後,對isDone的後續調用將始終返回true。如果此方法返回true,則隨後對isCancelled的調用將始終返回true。

因爲FutureTask中的cancel方法是實現Future接口中的cancel方法,所以它肯定也是嚴格遵守上面Future接口中cancel方法的規範:

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

首先看第一個if,如此時的state不是NEW狀態,則會直接返回false,這不就對應着前面所講的"如果此任務處於已經完成、已被取消過、或其他原因不能被取消這三種情況的一種,則此次取消操作失敗"嗎?

我們繼續看if中的代碼:

UNSAFE.compareAndSwapInt(this, stateOffset, NEW, 
                         mayInterruptIfRunning ? INTERRUPTING : CANCELLED

我們會根據傳入布爾值mayInterruptIfRunning來決定將NEW狀態置爲中間態INTERRUPTING或終止態CANCELLED,你看這裏是不是和前面講的run方法中finally塊中的內容對上了,是不是很爽。

然後繼續看try代碼塊中的內容:

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);          }
         }
 } finally {
          finishCompletion();
 }

前面講了,runner就是真正執行任務的線程,所以此時調用此線程的interrupt方法,最後在finnally塊中將狀態置爲INTERRUPTED,這裏我們需要注意的是,我們知道Thread的interrupt方法不一定會中斷線程,那大家可能會想,那這cancel方法還有啥用啊?因爲FutureTask是提供給我們獲取線程任務結果的,我們只要使FutureTask的結果爲null,管它任務真結束還是假結束。還記得run方法中的set方法嗎,只有當此時state爲NEW,纔會把任務執行結果賦值給outcome,但此時如果cancel方法中的if方法成功了,那states就不是NEW了,則outcome是不會被賦值的。所以是不是前後都串起來了?!

最後返回true給用戶告訴他執行cancel方法成功了(其實取沒取消真不一定,只是outcome的值爲null而已)

 

三.  實戰——燒水喝茶

 

最後我貼個例子,來看看我們FutureTask是怎麼用的,這裏我們採用大家都喜歡用的例子:燒水喝茶。

我們喝茶之前一般都會有準備工作,一是洗杯子,二是燒水,而且這兩個是可以同時進行的,當這兩步都完成後,我們就可以泡水喝茶了,下面我們就用java代碼實現這個例子,這個例子的關鍵就是運用FutureTask來獲取洗杯子和燒水線程的結果,當結果都爲ture時,我們才能喝茶。

大家運行完程序後會發現,我們的get方法是阻塞的,那有沒有不阻塞的方法?這個我後面會專門寫篇文章來講講。

package com.yy.demo14_callBack;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @Author: 24只羊
 * @Description:
 * @Date: 2020-02-23
 */
public class FutureTaskDemo {

    public static final int SLEEP_TIME = 10000;

    // 清洗杯子
    static class ClearCup implements Callable<Boolean> {

        @Override
        public Boolean call() throws Exception {
            System.out.println("洗杯子啦");
            Thread.sleep(SLEEP_TIME);
            System.out.println("杯子洗完啦");
            return true;
        }
    }

    // 燒熱水
    static class BoilWater implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            System.out.println("開始燒水啦");
            Thread.sleep(SLEEP_TIME);
            System.out.println("燒水完成案例");
            return true;
        }
    }

   static void drinkWater(boolean clearCupIsOk, boolean boilWaterIsOk) {
        if (clearCupIsOk && boilWaterIsOk) {
            System.out.println("可以泡茶喝啦");
        } else {
            if (!clearCupIsOk) {
                System.out.println("茶杯清洗失敗");
            }
            if (!boilWaterIsOk) {
                System.out.println("燒水失敗");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 建立清洗杯子線程
        Callable<Boolean> clearCup = new ClearCup();
        FutureTask<Boolean> cTask = new FutureTask(clearCup);
        Thread clearCupThread = new Thread(cTask);
        // 建立燒水線程
        Callable<Boolean> boilCup = new BoilWater();
        FutureTask<Boolean> bTask = new FutureTask(boilCup);
        Thread boilCupThread = new Thread(bTask);

        // 開啓兩個線程
        clearCupThread.start();
        boilCupThread.start();

        // 獲取線程結果
        boolean clearCupIsOk =  cTask.get();
        boolean boilWaterIsOk = bTask.get();

        //喝水
        drinkWater(clearCupIsOk, boilWaterIsOk);

    }

}

(完)

如果大家有什麼問題,可以加我公衆號,一起學習交流~

                                                      

 

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