最近在學習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種,而狀態的流轉過程一共有下面四種情況:
-
NEW -> COMPLETING -> NORMAL :任務正常執行並返回
-
NEW -> COMPLETING -> EXCEPTIONAL :執行中出現異常
-
NEW -> CANCELLED :任務執行過程中被取消,並且不響應中斷
-
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,因此得出兩點:
-
所以state只要不是NEW,就表明任務已經完成了
-
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方法,一共完成了下面幾件事:
-
runner初始化
-
調用callable對象的call方法執行任務
-
任務結束後將state置爲中間態COMPLETING,並任務結果賦值給outcome
-
將state置爲終止態NORMAL或EXCEPTIONAL
-
喚醒Treiber棧中的所有線程
-
將runner,callable置爲null
-
驗證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);
}
}
(完)
如果大家有什麼問題,可以加我公衆號,一起學習交流~