線程池的執行任務有兩種方法,一種是 submit、一種是 execute;這兩個方法是有區別的,那么基于這個區別我們再來看看。
execute 和 submit 區別
execute 只可以接收一個 Runnable 的參數
execute 如果出現異常會拋出
execute 沒有返回值
submit 可以接收 Runable 和 Callable 這兩種類型的參數,
對于 submit 方法,如果傳入一個 Callable,可以得到一個 Future 的返回值
submit 方法調用不會拋異常,除非調用 Future.get
這里,我們重點了解一下 Callable/Future,可能很多同學知道他是一個帶返回值的線程,但是具體的實現可能不清楚。
Callable/Future 案例演示
Callable/Future 和 Thread 之類的線程構建最大的區別在于,能夠很方便的獲取線程執行完以后的結果。首先來看一個簡單的例子
public class CallableDemo implements Callable<String> {
public String call() throws Exception {
Thread.sleep(3000);//阻塞案例演示
return "hello world";
}
public static void main(String[] args) throws ExecutionException,
InterruptedException {
CallableDemo callableDemo = new CallableDemo();
FutureTask futureTask = new FutureTask(callableDemo);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
想一想我們為什么需要使用回調呢?那是因為結果值是由另一線程計算的,當前線程是不知道結果值什么時候計算完成,所以它傳遞一個回調接口給計算線程,當計算完成時,調用這個回調接口,回傳結果值。
這個在很多地方有用到,比如 Dubbo 的異步調用,比如消息中間件的異步通信等等… 利用 FutureTask、 Callable、 Thread 對耗時任務(如查詢數據庫)做預處理,在需要計算結果之前就啟動計算。
所以我們來看一下 Future/Callable 是如何實現的
Callable/Future 原理分析
在剛剛實現的 demo 中,我們用到了兩個 api,分別是 Callable 和 FutureTask。Callable 是一個函數式接口,里面就只有一個 call 方法。子類可以重寫這個方法,并且這個方法會有一個返回值
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
FutureTask
FutureTask 的類關系圖如下,它實現 RunnableFuture 接口,那么這個 RunnableFuture 接口的作用是什么呢。
在講解 FutureTask 之前,先看看 Callable, Future, FutureTask 它們之間的關系圖,如下:
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 是一個接口,它繼承了 Runnable 和 Future 這兩個接口, Runnable 太熟悉了, 那么 Future 是什么呢?
Future 表示一個任務的生命周期,并提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
// 當前的 Future 是否被取消,返回 true 表示已取消
boolean isCancelled();
// 當前 Future 是否已結束。包括運行完成、拋出異常以及取消,都表示當前 Future 已結束
boolean isDone();
// 獲取 Future 的結果值。如果當前 Future 還沒有結束,那么當前線程就等待,
// 直到 Future 運行結束,那么會喚醒等待結果值的線程的。
V get() throws InterruptedException, ExecutionException;
// 獲取 Future 的結果值。與 get()相比較多了允許設置超時時間
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
分析到這里我們其實有一些初步的頭緒了, FutureTask 是 Runnable 和 Future 的結合,如果我們把 Runnable 比作是生產者, Future 比作是消費者,那么 FutureTask 是被這兩者共享的,生產者運行 run 方法計算結果,消費者通過 get 方法獲取結果。
作為生產者消費者模式,有一個很重要的機制,就是如果生產者數據還沒準備的時候,消費者會被阻塞。當生產者數據準備好了以后會喚醒消費者繼續執行。
這個有點像我們上次可分析的阻塞隊列,那么在 FutureTask 里面是基于什么方式實現的呢?
state 的含義
表示 FutureTask 當前的狀態,分為七種狀態
private static final int NEW = 0; // NEW 新建狀態,表示這個 FutureTask還沒有開始運行
// COMPLETING 完成狀態, 表示 FutureTask 任務已經計算完畢了
// 但是還有一些后續操作,例如喚醒等待線程操作,還沒有完成。
private static final int COMPLETING = 1;
// FutureTask 任務完結,正常完成,沒有發生異常
private static final int NORMAL = 2;
// FutureTask 任務完結,因為發生異常。
private static final int EXCEPTIONAL = 3;
// FutureTask 任務完結,因為取消任務
private static final int CANCELLED = 4;
// FutureTask 任務完結,也是取消任務,不過發起了中斷運行任務線程的中斷請求
private static final int INTERRUPTING = 5;
// FutureTask 任務完結,也是取消任務,已經完成了中斷運行任務線程的中斷請求
private static final int INTERRUPTED = 6;
run 方法
public void run() {
// 如果狀態 state 不是 NEW,或者設置 runner 值失敗
// 表示有別的線程在此之前調用 run 方法,并成功設置了 runner 值
// 保證了只有一個線程可以運行 try 代碼塊中的代碼。
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
//只有c不為null且狀態state為NEW的情況
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//調用callable的call方法,并獲得返回結果
result = c.call();
//運行成功
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
//設置結果
set(result);
}
} 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);
}
}
其實 run 方法作用非常簡單,就是調用 callable 的 call 方法返回結果值 result,根據是否發生異常,調用 set(result)或 setException(ex)方法表示 FutureTask 任務完結。
不過因為 FutureTask 任務都是在多線程環境中使用,所以要注意并發沖突問題。注意在 run方法中,我們沒有使用 synchronized 代碼塊或者 Lock 來解決并發問題,而是使用了 CAS 這個樂觀鎖來實現并發安全,保證只有一個線程能運行 FutureTask 任務。
get 方法
get 方法就是阻塞獲取線程執行結果,這里主要做了兩個事情
- 判斷當前的狀態,如果狀態小于等于 COMPLETING,表示 FutureTask 任務還沒有完結,所以調用 awaitDone 方法,讓當前線程等待。
- report 返回結果值或者拋出異常
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
awaitDone
如果當前的結果還沒有被執行完,把當前線程線程和插入到等待隊列
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
FutureTask.WaitNode q = null;
boolean queued = false;// 節點是否已添加
for (;;) {
// 如果當前線程中斷標志位是 true,
// 那么從列表中移除節點 q,并拋出 InterruptedException 異常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 當狀態大于 COMPLETING 時,表示FutureTask任務已結束
if (s > COMPLETING) {
if (q != null)
// 將節點 q 線程設置為 null,因為線程沒有阻塞等待
q.thread = null;
return s;
}
// 表示還有一些后序操作沒有完成,那么當前線程讓出執行權
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//表示狀態是 NEW,那么就需要將當前線程阻塞等待。
// 就是將它插入等待線程鏈表中
else if (q == null)
q = new FutureTask.WaitNode();
// 使用 CAS 函數將新節點添加到鏈表中,如果添加失敗,那么queued 為 false,
// 下次循環時,會繼續添加,知道成功。
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
// timed 為 true 表示需要設置超時
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
// 讓當前線程等待 nanos 時間
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}
被阻塞的線程,會等到 run 方法執行結束之后被喚醒
report
report 方法就是根據傳入的狀態值 s,來決定是拋出異常,還是返回結果值。 這個兩種情況都表示 FutureTask 完結了
private V report(int s) throws ExecutionException {
Object x = outcome;//表示 call 的返回值
if (s == NORMAL) // 表示正常完結狀態,所以返回結果值
return (V)x;
// 大于或等于 CANCELLED,都表示手動取消 FutureTask 任務,
// 所以拋出 CancellationException 異常
if (s >= CANCELLED)
throw new CancellationException();
// 否則就是運行過程中,發生了異常,這里就拋出這個異常
throw new ExecutionException((Throwable)x);
}
線程池對于 Future/Callable 的執行
我們現在再來看線程池里面的 submit 方法,就會很清楚了。
public class CallableDemo implements Callable<String> {
public String call() throws Exception {
Thread.sleep(3000);//阻塞案例演示
return "hello world";
}
public static void main(String[] args) throws ExecutionException,
InterruptedException {
ExecutorService es= Executors.newFixedThreadPool(1);
CallableDemo callableDemo = new CallableDemo();
FutureTask futureTask = new FutureTask(callableDemo);
Future future=es.submit(callableDemo);
System.out.println(futureTask.get());
}
}
AbstractExecutorService.submit
調用抽象類中的 submit 方法,這里其實相對于 execute 方法來說,只多做了一步操作,就是封裝了一個 RunnableFuture
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
newTaskFor
更簡單
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
然后調用 execute 方法,這里面的邏輯前面分析過了,會通過 worker 線程來調用過 ftask 的run 方法。而這個 ftask 其實就是 FutureTask 里面最終實現的邏輯。