原始碼|使用FutureTask的正確姿勢

monkeysayhi發表於2019-01-12

執行緒池的實現核心之一是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 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章