執行緒池的實現核心之一是FutureTask。在提交任務時,使用者實現的Callable例項task會被包裝為FutureTask例項ftask;提交後任務非同步執行,無需使用者關心;當使用者需要時,再呼叫FutureTask#get()獲取結果——或異常。
隨之而來的問題是,**如何優雅的獲取ftask的結果並處理異常?**本文討論使用FutureTask的正確姿勢。
JDK版本:oracle java 1.8.0_102
今天換個風格。
原始碼分析
從提交一個Callable例項task開始。
submit()
ThreadPoolExecutor直接繼承AbstractExecutorService的實現。
public abstract class AbstractExecutorService implements ExecutorService {
...
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
...
}
複製程式碼
後續流程可參考原始碼|從序列執行緒封閉到物件池、執行緒池。最終會在ThreadPoolExecutor#runWorker()中執行task.run()。
task即5行建立的ftask,看newTaskFor()。
newTaskFor()
AbstractExecutorService#newTaskFor()建立一個RunnableFuture型別的FutureTask。
public abstract class AbstractExecutorService implements ExecutorService {
...
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
...
}
複製程式碼
看FutureTask的實現。
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {
...
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;
...
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
...
}
複製程式碼
構造方法的重點是初始化ftask狀態為NEW。
狀態機
狀態轉換比較少,直接給狀態序列:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
複製程式碼
狀態在後面有用.
run()
簡化如下:
public class FutureTask<V> implements RunnableFuture<V> {
...
public void run() {
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(ex);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
...
}
複製程式碼
如果執行時未丟擲異常
如果未丟擲異常,則ran==true,FutureTask#set()設定結果。
public class FutureTask<V> implements RunnableFuture<V> {
...
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
...
}
複製程式碼
- outcome中儲存結果result
- 連續兩步設定狀態到NORMAL
- finishCompletion()執行一些清理
記住outcome。
相當於4行獲取獨佔鎖,5-6行執行鎖中的操作(注意,7行是不加鎖的)。
如果執行時丟擲了異常
如果執行時丟擲了異常,則被12行catch捕獲,FutureTask#setException()設定結果;同時,ran==false,因此不執行FutureTask#set()。
public class FutureTask<V> implements RunnableFuture<V> {
...
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
...
}
複製程式碼
- outcome中儲存異常t
- 連續兩步設定狀態到EXCEPTIONAL
- finishCompletion()執行一些清理
如果沒有丟擲異常在,則outcome記錄正常結果;如果丟擲了異常,則outcome記錄異常。
如果認為正常結果和異常都屬於“任務的輸出”,則使用相同的變數outcome記錄是合理的;同時,使用不同的結束狀態區分outcome中記錄的內容。
run()小結
FutureTask將使用者實現的task封裝為ftask,使用狀態機和outcome管理ftask的執行過程。這些過程對使用者是不可見的,直到使用者呼叫get()方法。
順道明白了Callable例項是如何執行的,為什麼實現Callable#call()方法時可以將受檢異常拋到外層(而Runable#run()方法則必須在方法內處理,不能丟擲)。
get()
public class FutureTask<V> implements RunnableFuture<V> {
...
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
...
}
複製程式碼
- 5行利用定義狀態的實際值判斷ftask是否已完成,如果未完成(NEW、COMPLETING),則wait阻塞直到完成,該過程可丟擲InterruptedException退出。
- 待ftask完成後,呼叫report()報告結束狀態。
5行的寫法不可讀,摒棄。
report()
public class FutureTask<V> implements RunnableFuture<V> {
...
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);
}
...
}
複製程式碼
- 如果結束狀態為NORMAL,則outcome儲存了正常結果,泛型強轉,返回。
- 7行利用定義狀態的實際值判斷ftask是否是被取消導致結束的(CANCELLED、INTERRUPTING、INTERRUPTED),如果是,則將丟擲CancellationException。
- 如果不是被取消的,就是執行過程中task自己丟擲了異常,則outcome儲存了該異常t,包裝返回ExecutionException。
將異常t作為ExecutionException的cause包裝起來,異常閱讀方法參考你真的會閱讀Java的異常資訊嗎?。
CancellationException和ExecutionException
- CancellationException是非受檢異常,原則上可以不處理,但仍然建議處理。
- ExecutionException是受檢異常,在外層必須處理。
原始碼小結
- 實現Callable#.call()時可以將受檢異常拋到外層。
- 不管實現Callable#.call()時是否丟擲了受檢異常,都要在FutureTask#get()時捕獲ExecutionException;建議捕獲CancellationException。
- FutureTask#get()中呼叫了阻塞方法,因此還需要捕獲InterruptedException。
- CancellationException異常中不會給出取消原因,包括是否因為被中斷。
- 工程上建議使用超時版的FutureTask#get(),超時會丟擲TimeoutException,需要處理。
反觀Future#get()的API宣告:
public interface Future<V> {
...
/**
* Waits if necessary for the computation to complete, and then
* retrieves its result.
*
* @return the computed result
* @throws CancellationException if the computation was cancelled
* @throws ExecutionException if the computation threw an
* exception
* @throws InterruptedException if the current thread was interrupted
* while waiting
*/
V get() throws InterruptedException, ExecutionException;
...
}
複製程式碼
right。
一種正確姿勢
給出一種比較全面的正確姿勢,僅供參考。
int timeoutSec = 30;
try {
MyResult result = ftask.get(timeoutSec, TimeUnit.SECONDS);
} catch (ExecutionException e) {
Throwable t = e.getCause();
// handle some checked exceptions
if (t instantanceof IOExcaption) {
xxx;
} else if (...) {
xxx;
} else { // handle remained checked exceptions and unchecked exceptions
throw new RuntimeException("xxx", t);
}
} catch (CancellationException e) {
xxx;
throw new UnknownException(String.format("Task %s canceled unexpected", taskId));
} catch (TimeoutException e) {
xxx;
LOGGER.error(String.format("Timeout for %ds, trying to cancel task: %s", timeoutSec, taskId));
ftask.cancel();
LOGGER.debug(String.format("Succeed to cancel task: %s" % taskId));
} catch (InterruptedException e) {
xxx;
}
複製程式碼
- 根據實際需求刪減。
- 猴子喜歡在一些語義模糊的地方加assert或丟擲UnknownException代替註釋。
- 對InterruptedException的處理暫時不討論(少有的用於控制流程的異常,猴子理解的有點模糊),讀者可參考處理 InterruptedException。
換風格不錯,寫起來快多了。
本文連結:原始碼|使用FutureTask的正確姿勢
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。