前陣子在用C++ 98(是比較落后了,嗯,C++11原生支持Future)開發的時候,對脫離業務的公共邏輯抽象出來了一個簡單的任務執行框架,里面主要是線程池和一些同步異步的任務。在開發異步任務的時候,為了實現類似java Future模式的能力,對實現方式考量了好久,最終使用了信號量這么重的東西來實現了Future的能力,同時也不禁對java的Future實現產生興趣,java的Future是怎么實現的呢?
concurrent 包
java Future相關的代碼基本都在java.util.concurrent
的包里面,Future.java
是一個接口,定義了最基本的一些任務操作和狀態判斷。
// Future.java
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit);
我們從FutureTask.java
去了解Future
的內部機制,FutureTask
對Future
有比較接地氣的實現,其他的實現或多或少都加入了新的一些特性,對了解原理沒太大幫助。
FutureTask.java
是對Futre
和Runnable
最簡單的實現,所以FutureTask
是一個可執行的異步對象。
FutureTask的狀態
FutureTask
定義了7種狀態,這7種狀態里面包含了簡單的狀態機,使用了一個用volatile
修飾的int
來記錄狀態。如下:
/** Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
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;
狀態的變換基本在FutureTask.java
的所有函數中都有體現,我們來看看幾個典型的狀態變換:
-
構造函數
構造函數完成時,將狀態置為
NEW
-
取消操作
取消操作對任務狀態進行判斷。
如果任務正在執行但沒有完成時,發出中斷,并將任務狀態置為中斷中狀態,并在執行線程完成后,置為中斷完成狀態
NEW -> INTERRUPTING -> INTERRUPTED
當任務還沒有執行,則直接置為取消狀態
NEW -> CANCELLED
-
執行操作
執行分執行成功和執行異常兩種,狀態轉換路徑是這兩個。
NEW -> COMPLETING -> NORMAL
和NEW -> COMPLETING -> EXCEPTIONAL
FutureTask的執行
FutureTask
因為封裝了Runnable
的接口,實現了run
函數,所以可以直接執行,直接執行使用主線程;FutureTask
的另外一種執行方式是提交到線程池去執行,由線程池去分配執行線程;只有提交到線程池去執行才能體現異步的特性。不過我們不關注執行的方式,我們關注執行的邏輯。
FutureTask
的構造函數傳入了Callable
或Runnable
對象,也即是需要執行的業務邏輯,他是業務邏輯的基本表現形式,保存在類屬性callable
,在run
函數里面,調用callalbe.call()
來執行業務邏輯。run
函數主要完成以下幾個操作。
- 判斷當前狀態是否為
NEW
,如果不是,說明任務被執行過,或者已被取消,直接返回。 - 如果狀態為
NEW
,接著會通過unsafe
類把任務執行線程保存在runner
字段中,如果保存失敗,則直接返回。 - 執行任務
- 任務執行成功,
set
保存結果,不成功setException
保存異常信息,任務的狀態在這里改變。
以下是run
函數的邏輯。
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread())) // 1, 2
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call(); // 3
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex); // 4
}
if (ran)
set(result); // 4
}
} 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);
}
}
由于在任務執行的過程中,可能被取消,所以在finally
塊里,會任務的根據狀態來做一些善后的工作。
FutureTask的取消
任務的取消比較簡單,我們知道,在執行的時候,執行任務的線程會保存在runner
屬性中,所以對于正在執行的任務,取消的本質就是將執行的線程取出來,向該線程發出interupt
信號。但對于一個較為完備的取消動作,cancel
做了一下幾個動作。
判斷任務當前執行狀態,如果任務狀態不為
NEW
,則說明任務或者已經執行完成,或者執行異常,不能被取消,直接返回false
表示執行失敗。-
判斷需要中斷任務執行線程,則
- 把任務狀態從
NEW
轉化到INTERRUPTING
。這是個中間狀態。 - 中斷任務執行線程。
- 修改任務狀態為
INTERRUPTED
。
- 把任務狀態從
如果不需要中斷任務執行線程,直接把任務狀態從
NEW
轉化為CANCELLED
。如果轉化失敗則返回false
表示取消失敗。-
調用
finishCompletion
這個函數將阻塞在等待這個任務完成的線程喚醒,具體操作是
LockSupport.unpark(t)
,這些線程都是在awaitDone
函數內的LockSupport.park(this)
中阻塞的,關于awaitDone
函數的作用后文還會繼續介紹。finishCompletion
除了在這里有調用以外,在set
和setException
中也有調用。以下是
finishCompletion
的實現/** * Removes and signals all waiting threads, invokes done(), and * nulls out callable. */ 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 }
以下是cancel
函數的實現。
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) // 1, 2
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); // 3
}
}
} finally {
finishCompletion(); // 4
}
return true;
}
FutureTask的結果
異步任務提交到線程池去執行后,由于無法預知什么時候結束,所以必須得提供接口來獲取任務執行的結果,在FutureTask
中,get
函數用來獲取任務執行的結果。該函數有了設置超時和不超時的兩種實現。
除去超時的差異,get
操作對任務的狀態進行判斷,當狀態還沒有完成的時候,調用awaitDone
函數來等待完成,我們在上面提到過這個函數,對于未完成的任務awaitDone
阻塞,而finishCompletion
喚醒阻塞。
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
我們來詳細看看awaitDone函數做了什么操作,它是如何阻塞的,它是怎么設置超時和完成返回的。
awaitDone
主題是一個死循環,輪詢判斷任務的狀態。
當執行的線程被中斷時,調用
removeWaiter
移除等待節點WaitNode
,拋出中斷異常當狀態為已經完成,直接返回
當狀態為完成中,通過
Thread.yield()
讓出CPU時間如果當前線程還沒有創建
WaitNode
等待節點保存到等待隊列里面去,則新建一個等待節點,插入到等待鏈表,表明當前線程也準備進入等待該任務完成的隊列中去。-
最后是進入阻塞的動作,通過
LockSupport.park
,如果設置了超時的時間,則將時間作為參數傳遞到park
中去。綜上,
awaitDone
函數除了對狀態的判斷以外,核心就是LockSupport
的阻塞等待完成的操作了。我們后面還會探討一下LockSupport
。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); } }
當awaitDone
的阻塞完成以后,就會將結果返回,將結果返回是通過report
函數來實現的,返回的是執行完成的結果或者是執行中獲得的異常信息。
LockSupport
異步執行任務,在獲取任務狀態時,阻塞是必然的,在開頭引子我簡單的提到了在自己實現的那個簡陋的框架內,使用了信號量的方式來設置異步任務的阻塞和執行狀態。在這里是通過LockSupport
來實現的,阻塞(park)和喚醒(unpark)。
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
可以看到LockSupport
則是通過UNSAFE
的同名函數來實現的。java不能直接訪問操作系統底層,而是通過本地方法來訪問。Unsafe類提供了硬件級別的原子操作。
Unsafe類是在sun.misc包下,不屬于java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基于Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運行效率,增強Java語言底層操作能力方面起了很大的作用。Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,因此Java官方并不建議使用的,官方文檔也幾乎沒有。
所以在這里java也是使用了C級別的線程同步機制來完成這些操作的,在這就不再展開了。
為保持文章內容的連貫性,部分內容參考自: