JDK1.8源碼分析項目(中文注釋)Github地址:
https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs
1 Future是什么?
先舉個例子,我們平時網購買東西,下單后會生成一個訂單號,然后商家會根據這個訂單號發貨,發貨后又有一個快遞單號,然后快遞公司就會根據這個快遞單號將網購東西快遞給我們。在這一過程中,這一系列的單號都是我們收貨的重要憑證。
因此,JDK的Future就類似於我們網購買東西的單號,當我們執行某一耗時的任務時,我們可以另起一個線程異步去執行這個耗時的任務,同時我們可以干點其他事情。當事情干完后我們再根據future這個"單號"去提取耗時任務的執行結果即可。因此Future也是多線程中的一種應用模式。
擴展: 說起多線程,那么Future又與Thread有什么區別呢?最重要的區別就是Thread是沒有返回結果的,而Future模式是有返回結果的。
2 如何使用Future
前面搞明白了什么是Future,下面我們再來舉個簡單的例子看看如何使用Future。
假如現在我們要打火鍋,首先我們要准備兩樣東西:把水燒開和准備食材。因為燒開水是一個比較漫長的過程(相當於耗時的業務邏輯),因此我們可以一邊燒開水(相當於另起一個線程),一邊准備火鍋食材(主線程),等兩者都准備好了我們就可以開始打火鍋了。
// DaHuoGuo.java
public class DaHuoGuo {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + ":" + "開始燒開水...");
// 模擬燒開水耗時
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":" + "開水已經燒好了...");
return "開水";
}
});
Thread thread = new Thread(futureTask);
thread.start();
// do other thing
System.out.println(Thread.currentThread().getName() + ":" + " 此時開啟了一個線程執行future的邏輯(燒開水),此時我們可以干點別的事情(比如准備火鍋食材)...");
// 模擬准備火鍋食材耗時
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + ":" + "火鍋食材准備好了");
String shicai = "火鍋食材";
// 開水已經稍好,我們取得燒好的開水
String boilWater = futureTask.get();
System.out.println(Thread.currentThread().getName() + ":" + boilWater + "和" + shicai + "已經准備好,我們可以開始打火鍋啦");
}
}
執行結果如下截圖,符合我們的預期:
從以上代碼中可以看到,我們使用Future主要有以下步驟:
- 新建一個
Callable
匿名函數實現類對象,我們的業務邏輯在Callable
的call
方法中實現,其中Callable的泛型是返回結果類型; - 然后把
Callable
匿名函數對象作為FutureTask
的構造參數傳入,構建一個futureTask
對象; - 然后再把
futureTask
對象作為Thread
構造參數傳入並開啟這個線程執行去執行業務邏輯; - 最后我們調用
futureTask
對象的get
方法得到業務邏輯執行結果。
可以看到跟Future使用有關的JDK類主要有FutureTask
和Callable
兩個,下面主要對FutureTask
進行源碼分析。
擴展: 還有一種使用
Future
的方式是將Callable
實現類提交給線程池執行的方式,這里不再介紹,自行百度即可。
3 FutureTask類結構分析
我們先來看下FutureTask
的類結構:
可以看到FutureTask
實現了RunnableFuture
接口,而RunnableFuture
接口又繼承了Future
和Runnable
接口。因為FutureTask
間接實現了Runnable
接口,因此可以作為任務被線程Thread
執行;此外,最重要的一點就是FutureTask
還間接實現了Future
接口,因此還可以獲得任務執行的結果。下面我們就來簡單看看這幾個接口的相關api
。
// Runnable.java
@FunctionalInterface
public interface Runnable {
// 執行線程任務
public abstract void run();
}
Runnable
沒啥好說的,相信大家都已經很熟悉了。
// Future.java
public interface Future<V> {
/**
* 嘗試取消線程任務的執行,分為以下幾種情況:
* 1)如果線程任務已經完成或已經被取消或其他原因不能被取消,此時會失敗並返回false;
* 2)如果任務還未開始執行,此時執行cancel方法,那么任務將被取消執行,此時返回true;TODO 此時對應任務狀態state的哪種狀態???不懂!!
* 3)如果任務已經開始執行,那么mayInterruptIfRunning這個參數將決定是否取消任務的執行。
* 這里值得注意的是,cancel(true)實質並不能真正取消線程任務的執行,而是發出一個線程
* 中斷的信號,一般需要結合Thread.currentThread().isInterrupted()來使用。
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* 判斷任務是否被取消,在執行任務完成前被取消,此時會返回true
*/
boolean isCancelled();
/**
* 這個方法不管任務正常停止,異常還是任務被取消,總是返回true。
*/
boolean isDone();
/**
* 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
*/
V get() throws InterruptedException, ExecutionException;
/**
* 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
* 只不過在規定的時間內未獲取到結果,此時會拋出超時異常
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future
接口象征着異步執行任務的結果即執行一個耗時任務完全可以另起一個線程執行,然后此時我們可以去做其他事情,做完其他事情我們再調用Future.get()
方法獲取結果即可,此時若異步任務還沒結束,此時會一直阻塞等待,直到異步任務執行完獲取到結果。
// RunnableFuture.java
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
RunnableFuture
是Future
和Runnable
接口的組合,即這個接口表示又可以被線程異步執行,因為實現了Runnable
接口,又可以獲得線程異步任務的執行結果,因為實現了Future
接口。因此解決了Runnable
異步任務沒有返回結果的缺陷。
接下來我們來看下FutureTask
,FutureTask
實現了RunnableFuture
接口,因此是Future
和Runnable
接口的具體實現類,是一個可被取消的異步線程任務,提供了Future
的基本實現,即異步任務執行后我們能夠獲取到異步任務的執行結果,是我們接下來分析的重中之重。FutureTask
可以包裝一個Callable
和Runnable
對象,此外,FutureTask
除了可以被線程執行外,還可以被提交給線程池執行。
我們先看下FutureTask
類的api
,其中重點方法已經紅框框出。
上圖中FutureTask
的run
方法是被線程異步執行的方法,get
方法即是取得異步任務執行結果的方法,還有cancel
方法是取消任務執行的方法。接下來我們主要對這三個方法進行重點分析。
思考:
FutureTask
覆寫的run
方法的返回類型依然是void
,表示沒有返回值,那么FutureTask
的get
方法又是如何獲得返回值的呢?FutureTask
的cancel
方法能真正取消線程異步任務的執行么?什么情況下能取消?
因為FutureTask
異步任務執行結果還跟Callable
接口有關,因此我們再來看下Callable
接口:
// Callable.java
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*/
V call() throws Exception;
}
我們都知道,Callable<V>
接口和Runnable
接口都可以被提交給線程池執行,唯一不同的就是Callable<V>
接口是有返回結果的,其中的泛型V
就是返回結果,而Runnable
接口是沒有返回結果的。
思考: 一般情況下,
Runnable
接口實現類才能被提交給線程池執行,為何Callable
接口實現類也可以被提交給線程池執行?想想線程池的submit
方法內部有對Callable
做適配么?
4 FutureTask源碼分析
4.1 FutureTask成員變量
我們首先來看下FutureTask
的成員變量有哪些,理解這些成員變量對后面的源碼分析非常重要。
// FutureTask.java
/** 封裝的Callable對象,其call方法用來執行異步任務 */
private Callable<V> callable;
/** 在FutureTask里面定義一個成員變量outcome,用來裝異步任務的執行結果 */
private Object outcome; // non-volatile, protected by state reads/writes
/** 用來執行callable任務的線程 */
private volatile Thread runner;
/** 線程等待節點,reiber stack的一種實現 */
private volatile WaitNode waiters;
/** 任務執行狀態 */
private volatile int state;
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 對應成員變量state的偏移地址
private static final long stateOffset;
// 對應成員變量runner的偏移地址
private static final long runnerOffset;
// 對應成員變量waiters的偏移地址
private static final long waitersOffset;
這里我們要重點關注下FutureTask
的Callable
成員變量,因為FutureTask
的異步任務最終是委托給Callable
去實現的。
思考:
FutureTask
的成員變量runner
,waiters
和state
都被volatile
修飾,我們可以思考下為什么這三個成員變量需要被volatile
修飾,而其他成員變量又不用呢?volatile
關鍵字的作用又是什么呢?- 既然已經定義了成員變量
runner
,waiters
和state
了,此時又定義了stateOffset
,runnerOffset
和waitersOffset
變量分別對應runner
,waiters
和state
的偏移地址,為何要多此一舉呢?
我們再來看看stateOffset
,runnerOffset
和waitersOffset
變量這三個變量的初始化過程:
// FutureTask.java
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = FutureTask.class;
stateOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("state"));
runnerOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("runner"));
waitersOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("waiters"));
} catch (Exception e) {
throw new Error(e);
}
}
4.2 FutureTask的狀態變化
前面講了FutureTask
的成員變量,有一個表示狀態的成員變量state
我們要重點關注下,state
變量表示任務執行的狀態。
// FutureTask.java
/** 任務執行狀態 */
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;
/** 任務被取消狀態,對應cancel(false) */
private static final int CANCELLED = 4;
/** 任務中斷狀態,是一個瞬間過渡狀態 */
private static final int INTERRUPTING = 5;
/** 任務被中斷狀態,對應cancel(true) */
private static final int INTERRUPTED = 6;
可以看到任務狀態變量state
有以上7種狀態,0-6分別對應着每一種狀態。任務狀態一開始是NEW
,然后由FutureTask
的三個方法set
,setException
和cancel
來設置狀態的變化,其中狀態變化有以下四種情況:
NEW -> COMPLETING -> NORMAL
:這個狀態變化表示異步任務的正常結束,其中COMPLETING
是一個瞬間臨時的過渡狀態,由set
方法設置狀態的變化;NEW -> COMPLETING -> EXCEPTIONAL
:這個狀態變化表示異步任務執行過程中拋出異常,由setException
方法設置狀態的變化;NEW -> CANCELLED
:這個狀態變化表示被取消,即調用了cancel(false)
,由cancel
方法來設置狀態變化;NEW -> INTERRUPTING -> INTERRUPTED
:這個狀態變化表示被中斷,即調用了cancel(true)
,由cancel
方法來設置狀態變化。
4.3 FutureTask構造函數
FutureTask
有兩個構造函數,我們分別來看看:
// FutureTask.java
// 第一個構造函數
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
可以看到,這個構造函數在我們前面舉的“打火鍋”的例子代碼中有用到,就是Callable
成員變量賦值,在異步執行任務時再調用Callable.call
方法執行異步任務邏輯。此外,此時給任務狀態state
賦值為NEW
,表示任務新建狀態。
我們再來看下FutureTask
的另外一個構造函數:
// FutureTask.java
// 另一個構造函數
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
這個構造函數在執行Executors.callable(runnable, result)
時是通過適配器RunnableAdapter
來將Runnable
對象runnable
轉換成Callable
對象,然后再分別給callable
和state
變量賦值。
注意,這里我們需要記住的是FutureTask
新建時,此時的任務狀態state
是NEW
就好了。
4.4 FutureTask.run方法,用來執行異步任務
前面我們有講到FutureTask
間接實現了Runnable
接口,覆寫了Runnable
接口的run
方法,因此該覆寫的run
方法是提交給線程來執行的,同時,該run
方法正是執行異步任務邏輯的方法,那么,執行完run
方法又是如何保存異步任務執行的結果的呢?
我們現在着重來分析下run
方法:
// FutureTask.java
public void run() {
// 【1】,為了防止多線程並發執行異步任務,這里需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:
// 1)若任務狀態state為NEW且runner為null,說明還未有線程執行過異步任務,此時滿足執行異步任務的條件,
// 此時同時調用CAS方法為成員變量runner設置當前線程的值;
// 2)若任務狀態state為NEW且runner不為null,任務狀態雖為NEW但runner不為null,說明有線程正在執行異步任務,
// 此時不滿足執行異步任務的條件,直接返回;
// 1)若任務狀態state不為NEW,此時不管runner是否為null,說明已經有線程執行過異步任務,此時沒必要再重新
// 執行一次異步任務,此時不滿足執行異步任務的條件;
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 拿到之前構造函數傳進來的callable實現類對象,其call方法封裝了異步任務執行的邏輯
Callable<V> c = callable;
// 若任務還是新建狀態的話,那么就調用異步任務
if (c != null && state == NEW) {
// 異步任務執行結果
V result;
// 異步任務執行成功還是始遍標志
boolean ran;
try {
// 【2】,執行異步任務邏輯,並把執行結果賦值給result
result = c.call();
// 若異步任務執行過程中沒有拋出異常,說明異步任務執行成功,此時設置ran標志為true
ran = true;
} catch (Throwable ex) {
result = null;
// 異步任務執行過程拋出異常,此時設置ran標志為false
ran = false;
// 【3】設置異常,里面也設置state狀態的變化
setException(ex);
}
// 【3】若異步任務執行成功,此時設置異步任務執行結果,同時也設置狀態的變化
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
// 異步任務正在執行過程中,runner一直是非空的,防止並發調用run方法,前面有調用cas方法做判斷的
// 在異步任務執行完后,不管是正常結束還是異常結束,此時設置runner為null
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
// 線程執行異步任務后的任務狀態
int s = state;
// 【4】如果執行了cancel(true)方法,此時滿足條件,
// 此時調用handlePossibleCancellationInterrupt方法處理中斷
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
可以看到執行異步任務的run
方法主要分為以下四步來執行:
- 判斷線程是否滿足執行異步任務的條件:為了防止多線程並發執行異步任務,這里需要判斷線程滿不滿足執行異步任務的條件;
- 若滿足條件,執行異步任務:因為異步任務邏輯封裝在
Callable.call
方法中,此時直接調用Callable.call
方法執行異步任務,然后返回執行結果; - 根據異步任務的執行情況做不同的處理:1) 若異步任務執行正常結束,此時調用
set(result);
來設置任務執行結果;2)若異步任務執行拋出異常,此時調用setException(ex);
來設置異常,詳細分析請見4.4.1小節
; - 異步任務執行完后的善后處理工作:不管異步任務執行成功還是失敗,若其他線程有調用
FutureTask.cancel(true)
,此時需要調用handlePossibleCancellationInterrupt
方法處理中斷,詳細分析請見4.4.2小節
。
這里值得注意的是判斷線程滿不滿足執行異步任務條件時,runner
是否為null
是調用UNSAFE
的CAS
方法compareAndSwapObject
來判斷和設置的,同時compareAndSwapObject
是通過成員變量runner
的偏移地址runnerOffset
來給runner
賦值的,此外,成員變量runner
被修飾為volatile
是在多線程的情況下, 一個線程的volatile
修飾變量的設值能夠立即刷進主存,因此值便可被其他線程可見。
4.4.1 FutureTask的set和setException方法
下面我們來看下當異步任務執行正常結束時,此時會調用set(result);
方法:
// FutureTask.java
protected void set(V v) {
// 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
// 【思考】此時任務不能被多線程並發執行,什么情況下會導致任務狀態不為NEW?
// 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什么都不需要做,
// 因此需要調用CAS方法來做判斷任務狀態是否為NEW
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 【2】將任務執行結果賦值給成員變量outcome
outcome = v;
// 【3】將任務狀態設置為NORMAL,表示任務正常結束
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
// 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
finishCompletion();
}
}
可以看到當異步任務正常執行結束后,且異步任務沒有被cancel
的情況下,此時會做以下事情:將任務執行結果保存到FutureTask
的成員變量outcome
中的,賦值結束后會調用finishCompletion
方法來喚醒阻塞的線程(哪里來的阻塞線程?后面會分析),值得注意的是這里對應的任務狀態變化是NEW -> COMPLETING -> NORMAL。
我們繼續來看下當異步任務執行過程中拋出異常,此時會調用setException(ex);
方法。
// FutureTask.java
protected void setException(Throwable t) {
// 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
// 【思考】此時任務不能被多線程並發執行,什么情況下會導致任務狀態不為NEW?
// 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什么都不需要做,
// 因此需要調用CAS方法來做判斷任務狀態是否為NEW
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 【2】將異常賦值給成員變量outcome
outcome = t;
// 【3】將任務狀態設置為EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
// 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
finishCompletion();
}
}
可以看到setException(Throwable t)
的代碼邏輯跟前面的set(V v)
幾乎一樣,不同的是任務執行過程中拋出異常,此時是將異常保存到FutureTask
的成員變量outcome
中,還有,值得注意的是這里對應的任務狀態變化是NEW -> COMPLETING -> EXCEPTIONAL。
因為異步任務不管正常還是異常結束,此時都會調用FutureTask
的finishCompletion
方法來喚醒喚醒阻塞的線程,這里阻塞的線程是指我們調用Future.get
方法時若異步任務還未執行完,此時該線程會阻塞。
// FutureTask.java
private void finishCompletion() {
// assert state > COMPLETING;
// 取出等待線程鏈表頭節點,判斷頭節點是否為null
// 1)若線程鏈表頭節點不為空,此時以“后進先出”的順序(棧)移除等待的線程WaitNode節點
// 2)若線程鏈表頭節點為空,說明還沒有線程調用Future.get()方法來獲取任務執行結果,固然不用移除
for (WaitNode q; (q = waiters) != null;) {
// 調用UNSAFE的CAS方法將成員變量waiters設置為空
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
// 取出WaitNode節點的線程
Thread t = q.thread;
// 若取出的線程不為null,則將該WaitNode節點線程置空,且喚醒正在阻塞的該線程
if (t != null) {
q.thread = null;
//【重要】喚醒正在阻塞的該線程
LockSupport.unpark(t);
}
// 繼續取得下一個WaitNode線程節點
WaitNode next = q.next;
// 若沒有下一個WaitNode線程節點,說明已經將所有等待的線程喚醒,此時跳出for循環
if (next == null)
break;
// 將已經移除的線程WaitNode節點的next指針置空,此時好被垃圾回收
q.next = null; // unlink to help gc
// 再把下一個WaitNode線程節點置為當前線程WaitNode頭節點
q = next;
}
break;
}
}
// 不管任務正常執行還是拋出異常,都會調用done方法
done();
// 因為異步任務已經執行完且結果已經保存到outcome中,因此此時可以將callable對象置空了
callable = null; // to reduce footprint
}
finishCompletion
方法的作用就是不管異步任務正常還是異常結束,此時都要喚醒且移除線程等待鏈表的等待線程節點,這個鏈表實現的是一個是Treiber stack
,因此喚醒(移除)的順序是"后進先出"即后面先來的線程先被先喚醒(移除),關於這個線程等待鏈表是如何成鏈的,后面再繼續分析。
4.4.2 FutureTask的handlePossibleCancellationInterrupt方法
在4.4小節
分析的run
方法里的最后有一個finally
塊,此時若任務狀態state >= INTERRUPTING
,此時說明有其他線程執行了cancel(true)
方法,此時需要讓出CPU
執行的時間片段給其他線程執行,我們來看下具體的源碼:
// FutureTask.java
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.
// 當任務狀態是INTERRUPTING時,此時讓出CPU執行的機會,讓其他線程執行
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
// assert state == INTERRUPTED;
// We want to clear any interrupt we may have received from
// cancel(true). However, it is permissible to use interrupts
// as an independent mechanism for a task to communicate with
// its caller, and there is no way to clear only the
// cancellation interrupt.
//
// Thread.interrupted();
}
思考: 為啥任務狀態是
INTERRUPTING
時,此時就要讓出CPU執行的時間片段呢?還有為什么要在義務任務執行后才調用handlePossibleCancellationInterrupt
方法呢?
4.5 FutureTask.get方法,獲取任務執行結果
前面我們起一個線程在其`run`方法中執行異步任務后,此時我們可以調用`FutureTask.get`方法來獲取異步任務執行的結果。
// FutureTask.java
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 【1】若任務狀態<=COMPLETING,說明任務正在執行過程中,此時可能正常結束,也可能遇到異常
if (s <= COMPLETING)
s = awaitDone(false, 0L);
// 【2】最后根據任務狀態來返回任務執行結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消
return report(s);
}
可以看到,如果任務狀態state<=COMPLETING
,說明異步任務正在執行過程中,此時會調用awaitDone
方法阻塞等待;當任務執行完后,此時再調用report
方法來報告任務結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消。
4.5.1 FutureTask.awaitDone方法
FutureTask.awaitDone
方法會阻塞獲取異步任務執行結果的當前線程,直到異步任務執行完成。
// FutureTask.java
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 計算超時結束時間
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 線程鏈表頭節點
WaitNode q = null;
// 是否入隊
boolean queued = false;
// 死循環
for (;;) {
// 如果當前獲取任務執行結果的線程被中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 【5】如果任務狀態>COMPLETING,此時返回任務執行結果,其中此時任務可能正常結束(NORMAL),可能拋出異常(EXCEPTIONAL)
// 或任務被取消(CANCELLED,INTERRUPTING或INTERRUPTED狀態的一種)
if (s > COMPLETING) {
// 【問】此時將當前WaitNode節點的線程置空,其中在任務結束時也會調用finishCompletion將WaitNode節點的thread置空,
// 這里為什么又要再調用一次q.thread = null;呢?
// 【答】因為若很多線程來獲取任務執行結果,在任務執行完的那一刻,此時獲取任務的線程要么已經在線程等待鏈表中,要么
// 此時還是一個孤立的WaitNode節點。在線程等待鏈表中的的所有WaitNode節點將由finishCompletion來移除(同時喚醒)所有
// 等待的WaitNode節點,以便垃圾回收;而孤立的線程WaitNode節點此時還未阻塞,因此不需要被喚醒,此時只要把其屬性置為
// null,然后其有沒有被誰引用,因此可以被GC。
if (q != null)
q.thread = null;
// 【重要】返回任務執行結果
return s;
}
// 【4】若任務狀態為COMPLETING,此時說明任務正在執行過程中,此時獲取任務結果的線程需讓出CPU執行時間片段
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 【1】若當前線程還沒有進入線程等待鏈表的WaitNode節點,此時新建一個WaitNode節點,並把當前線程賦值給WaitNode節點的thread屬性
else if (q == null)
q = new WaitNode();
// 【2】若當前線程等待節點還未入線程等待隊列,此時加入到該線程等待隊列的頭部
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);
}
// 【3】若沒有超時設置,此時直接阻塞當前線程
else
LockSupport.park(this);
}
}
FutureTask.awaitDone
方法主要做的事情總結如下:
- 首先
awaitDone
方法里面是一個死循環; - 若獲取結果的當前線程被其他線程中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException;
- 如果任務狀態
state>COMPLETING
,此時返回任務執行結果; - 若任務狀態為
COMPLETING
,此時獲取任務結果的線程需讓出CPU執行時間片段; - 若
q == null
,說明當前線程還未設置到WaitNode
節點,此時新建WaitNode
節點並設置其thread
屬性為當前線程; - 若
queued==false
,說明當前線程WaitNode
節點還未加入線程等待鏈表,此時加入該鏈表的頭部; - 當
timed
設置為true時,此時該方法具有超時功能,關於超時的邏輯這里不詳細分析; - 當前面6個條件都不滿足時,此時阻塞當前線程。
我們分析到這里,可以直到執行異步任務只能有一個線程來執行,而獲取異步任務結果可以多線程來獲取,當異步任務還未執行完時,此時獲取異步任務結果的線程會加入線程等待鏈表中,然后調用調用LockSupport.park(this);
方法阻塞當前線程。直到異步任務執行完成,此時會調用finishCompletion
方法來喚醒並移除線程等待鏈表的每個WaitNode
節點,這里這里喚醒(移除)WaitNode
節點的線程是從鏈表頭部開始的,前面我們也已經分析過。
還有一個特別需要注意的就是awaitDone
方法里面是一個死循環,當一個獲取異步任務的線程進來后可能會多次進入多個條件分支執行不同的業務邏輯,也可能只進入一個條件分支。下面分別舉兩種可能的情況進行說明:
情況1:
當獲取異步任務結果的線程進來時,此時異步任務還未執行完即state=NEW
且沒有超時設置時:
- 第一次循環:此時
q = null
,此時進入上面代碼標號【1】
的判斷分支,即為當前線程新建一個WaitNode
節點; - 第二次循環:此時
queued = false
,此時進入上面代碼標號【2】
的判斷分支,即將之前新建的WaitNode
節點加入線程等待鏈表中; - 第三次循環:此時進入上面代碼標號
【3】
的判斷分支,即阻塞當前線程; - 第四次循環:加入此時異步任務已經執行完,此時進入上面代碼標號
【5】
的判斷分支,即返回異步任務執行結果。
情況2:
當獲取異步任務結果的線程進來時,此時異步任務已經執行完即state>COMPLETING
且沒有超時設置時,此時直接進入上面代碼標號【5】
的判斷分支,即直接返回異步任務執行結果即可,也不用加入線程等待鏈表了。
4.5.2 FutureTask.report方法
在get
方法中,當異步任務執行結束后即不管異步任務正常還是異常結束,亦或是被cancel
,此時獲取異步任務結果的線程都會被喚醒,因此會繼續執行FutureTask.report
方法報告異步任務的執行情況,此時可能會返回結果,也可能會拋出異常。
// FutureTask.java
private V report(int s) throws ExecutionException {
// 將異步任務執行結果賦值給x,此時FutureTask的成員變量outcome要么保存着
// 異步任務正常執行的結果,要么保存着異步任務執行過程中拋出的異常
Object x = outcome;
// 【1】若異步任務正常執行結束,此時返回異步任務執行結果即可
if (s == NORMAL)
return (V)x;
// 【2】若異步任務執行過程中,其他線程執行過cancel方法,此時拋出CancellationException異常
if (s >= CANCELLED)
throw new CancellationException();
// 【3】若異步任務執行過程中,拋出異常,此時將該異常轉換成ExecutionException后,重新拋出。
throw new ExecutionException((Throwable)x);
}
4.6 FutureTask.cancel方法,取消執行任務
我們最后再來看下FutureTask.cancel
方法,我們一看到FutureTask.cancel
方法,肯定一開始就天真的認為這是一個可以取消異步任務執行的方法,如果我們這樣認為的話,只能說我們猜對了一半。
// FutureTask.java
public boolean cancel(boolean mayInterruptIfRunning) {
// 【1】判斷當前任務狀態,若state == NEW時根據mayInterruptIfRunning參數值給當前任務狀態賦值為INTERRUPTING或CANCELLED
// a)當任務狀態不為NEW時,說明異步任務已經完成,或拋出異常,或已經被取消,此時直接返回false。
// TODO 【問題】此時若state = COMPLETING呢?此時為何也直接返回false,而不能發出中斷異步任務線程的中斷信號呢??
// TODO 僅僅因為COMPLETING是一個瞬時態嗎???
// b)當前僅當任務狀態為NEW時,此時若mayInterruptIfRunning為true,此時任務狀態賦值為INTERRUPTING;否則賦值為CANCELLED。
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
// 【2】如果mayInterruptIfRunning為true,此時中斷執行異步任務的線程runner(還記得執行異步任務時就把執行異步任務的線程就賦值給了runner成員變量嗎)
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
// 中斷執行異步任務的線程runner
t.interrupt();
} finally { // final state
// 最后任務狀態賦值為INTERRUPTED
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
// 【3】不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點
} finally {
finishCompletion();
}
// 返回true
return true;
}
以上代碼中,當異步任務狀態state != NEW
時,說明異步任務已經正常執行完或已經異常結束亦或已經被cancel
,此時直接返回false
;當異步任務狀態state = NEW
時,此時又根據mayInterruptIfRunning
參數是否為true
分為以下兩種情況:
- 當
mayInterruptIfRunning = false
時,此時任務狀態state
直接被賦值為CANCELLED
,此時不會對執行異步任務的線程發出中斷信號,值得注意的是這里對應的任務狀態變化是NEW -> CANCELLED。 - 當
mayInterruptIfRunning = true
時,此時會對執行異步任務的線程發出中斷信號,值得注意的是這里對應的任務狀態變化是NEW -> INTERRUPTING -> INTERRUPTED。
最后不管mayInterruptIfRunning
為true
還是false
,此時都要調用finishCompletion
方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點。
從FutureTask.cancel
源碼中我們可以得出答案,該方法並不能真正中斷正在執行異步任務的線程,只能對執行異步任務的線程發出中斷信號。如果執行異步任務的線程處於sleep
、wait
或join
的狀態中,此時會拋出InterruptedException
異常,該線程可以被中斷;此外,如果異步任務需要在while
循環執行的話,此時可以結合以下代碼來結束異步任務線程,即執行異步任務的線程被中斷時,此時Thread.currentThread().isInterrupted()
返回true
,不滿足while
循環條件因此退出循環,結束異步任務執行線程,如下代碼:
public Integer call() throws Exception {
while (!Thread.currentThread().isInterrupted()) {
// 業務邏輯代碼
System.out.println("running...");
}
return 666;
}
注意:調用了FutureTask.cancel
方法,只要返回結果是true
,假如異步任務線程雖然不能被中斷,即使異步任務線程正常執行完畢,返回了執行結果,此時調用FutureTask.get
方法也不能夠獲取異步任務執行結果,此時會拋出CancellationException
異常。請問知道這是為什么嗎?
因為調用了FutureTask.cancel
方法,只要返回結果是true
,此時的任務狀態為CANCELLED
或INTERRUPTED
,同時必然會執行finishCompletion
方法,而finishCompletion
方法會喚醒獲取異步任務結果的線程等待列表的線程,而獲取異步任務結果的線程喚醒后發現狀態s >= CANCELLED
,此時就會拋出CancellationException
異常了。
5 總結
好了,本篇文章對FutureTask
的源碼分析就到此結束了,下面我們再總結下FutureTask
的實現邏輯:
- 我們實現
Callable
接口,在覆寫的call
方法中定義需要執行的業務邏輯; - 然后把我們實現的
Callable
接口實現對象傳給FutureTask
,然后FutureTask
作為異步任務提交給線程執行; - 最重要的是
FutureTask
內部維護了一個狀態state
,任何操作(異步任務正常結束與否還是被取消)都是圍繞着這個狀態進行,並隨時更新state
任務的狀態; - 只能有一個線程執行異步任務,當異步任務執行結束后,此時可能正常結束,異常結束或被取消。
- 可以多個線程並發獲取異步任務執行結果,當異步任務還未執行完,此時獲取異步任務的線程將加入線程等待列表進行等待;
- 當異步任務線程執行結束后,此時會喚醒獲取異步任務執行結果的線程,注意喚醒順序是"后進先出"即后面加入的阻塞線程先被喚醒。
- 當我們調用
FutureTask.cancel
方法時並不能真正停止執行異步任務的線程,只是發出中斷線程的信號。但是只要cancel
方法返回true
,此時即使異步任務能正常執行完,此時我們調用get
方法獲取結果時依然會拋出CancellationException
異常。
擴展: 前面我們提到了
FutureTask
的runner
,waiters
和state
都是用volatile
關鍵字修飾,說明這三個變量都是多線程共享的對象(成員變量),會被多線程操作,此時用volatile
關鍵字修飾是為了一個線程操作volatile
屬性變量值后,能夠及時對其他線程可見。此時多線程操作成員變量僅僅用了volatile
關鍵字仍然會有線程安全問題的,而此時Doug Lea老爺子沒有引入任何線程鎖,而是采用了Unsafe
的CAS
方法來代替鎖操作,確保線程安全性。
6 分析FutureTask源碼,我們能學到什么?
我們分析源碼的目的是什么?除了弄懂FutureTask
的內部實現原理外,我們還要借鑒大佬寫寫框架源碼的各種技巧,只有這樣,我們才能成長。
分析了FutureTask
源碼,我們可以從中學到:
- 利用
LockSupport
來實現線程的阻塞\喚醒機制; - 利用
volatile
和UNSAFE
的CAS
方法來實現線程共享變量的無鎖化操作; - 若要編寫超時異常的邏輯可以參考
FutureTask
的get(long timeout, TimeUnit unit)
的實現邏輯; - 多線程獲取某一成員變量結果時若需要等待時的線程等待鏈表的邏輯實現;
- 某一異步任務在某一時刻只能由單一線程執行的邏輯實現;
FutureTask
中的任務狀態satate
的變化處理的邏輯實現。- ...
以上列舉的幾點都是我們可以學習參考的地方。
若您覺得不錯,請無情的轉發和點贊吧!
【源碼筆記】Github地址:
https://github.com/yuanmabiji/Java-SourceCode-Blogs
公眾號【源碼筆記】,專注於Java后端系列框架的源碼分析。