最近工作中,遇到一個需求:300ms內請求到服務器返回值,否則取消請求。完成這個需求的時候,使用到了FutureTask。在這裏就記錄一下,並且研究一下其實現的原理。
文章順序:
- FutureTask的使用。
- 開發中可能出現的問題。
- 結合FutureTask的源碼分析問題。
- 精華部分
1. FutureTask的使用
在Java中,一般是通過繼承Thread類或者實現Runnable接口來創建多線程, Runnable接口不能返回結果,如果要獲取子線程的執行結果,一般都是在子線程執行結束之後,通過Handler將結果返回到調用線程,jdk1.5之後,Java提供了Callable接口來封裝子任務,Callable接口可以獲取返回結果。
Callable接口
下面可以看一下Callable接口的定義:
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;
}
Callable接口很簡單,是一個泛型接口,就是定義了一個call()方法,與Runnable的run()方法相比,這個有返回值,泛型V就是要返回的結果類型,可以返回子任務的執行結果。
Future接口
Future接口表示異步計算的結果,通過Future接口提供的方法,可以很方便的查詢異步計算任務是否執行完成,獲取異步計算的結果,取消未執行的異步任務,或者中斷異步任務的執行,接口定義如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
- cancel(boolean mayInterruptIfRunning):取消子任務的執行,如果這個子任務已經執行結束,或者已經被取消,或者不能被取消,這個方法就會執行失敗並返回false;如果子任務還沒有開始執行,那麼子任務會被取消,不會再被執行;如果子任務已經開始執行了,但是還沒有執行結束,根據mayInterruptIfRunning的值,如果mayInterruptIfRunning = true,那麼會中斷執行任務的線程,然後返回true,如果參數爲false,會返回true,不會中斷執行任務的線程。這個方法在FutureTask的實現中有很多值得關注的地方,後面再細說。
需要注意,這個方法執行結束,返回結果之後,再調用isDone()會返回true。 - isCancelled(),判斷任務是否被取消,如果任務執行結束(正常執行結束和發生異常結束,都算執行結束)前被取消,也就是調用了cancel()方法,並且cancel()返回true,則該方法返回true,否則返回false.
- isDone():判斷任務是否執行結束,正常執行結束,或者發生異常結束,或者被取消,都屬於結束,該方法都會返回true.
- V get():獲取結果,如果這個計算任務還沒有執行結束,該調用線程會進入阻塞狀態。如果計算任務已經被取消,調用get()會拋出CancellationException,如果計算過程中拋出異常,該方法會拋出ExecutionException,如果當前線程在阻塞等待的時候被中斷了,該方法會拋出InterruptedException。
- V get(long timeout, TimeUnit unit):帶超時限制的get(),等待超時之後,該方法會拋出TimeoutException。
FutureTask
FutureTask可以像Runnable一樣,封裝異步任務,然後提交給Thread或線程池執行,然後獲取任務執行結果。原因在於FutureTask實現了RunnableFuture接口,RunnableFuture是什麼呢,其實就是Runnable和Future的結合,它繼承自Runnable和Future。繼承關係如下:
public class FutureTask<V> implements RunnableFuture<V> {
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
FutureTask使用
1. FutureTask + Thread :上面介紹過,FutureTask有Runnable接口和Callable接口的特徵,可以被Thread執行。
//step1:封裝一個計算任務,實現Callable接口
class Task implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
try {
for (int i = 0; i < 10; i++) {
Log.d(TAG, "task..." + Thread.currentThread().getName() + "...i = " + i);
//模擬耗時操作
Thread.sleep(100);
}
} catch (InterruptedException e) {
Log.e(TAG, " is interrupted when calculating, will stop...");
return false; // 注意這裏如果不return的話,線程還會繼續執行,所以任務超時後在這裏處理結果然後返回
}
return true;
}
}
//step2:創建計算任務,作爲參數,傳入FutureTask
Task task = new Task();
FutureTask futureTask = new FutureTask(task);//step3:將FutureTask提交給Thread執行
Thread thread1 = new Thread(futureTask);
thread1.setName("task thread 1");
thread1.start();
//step4:獲取執行結果,由於get()方法可能會阻塞當前調用線程,如果子任務執行時間不確定,最好在子線程中獲取執行結果try {
// boolean result = (boolean) futureTask.get();
boolean result = (boolean) futureTask.get(5, TimeUnit.SECONDS);
Log.d(TAG, "result:" + result);
} catch (InterruptedException e) {
Log.e(TAG, "守護線程阻塞被打斷...");
e.printStackTrace();
} catch (ExecutionException e) {
Log.e(TAG, "執行任務時出錯...");
e.printStackTrace();
} catch (TimeoutException e) {
Log.e(TAG, "執行超時...");
futureTask.cancel(true);
e.printStackTrace();
} catch (CancellationException e) {
//如果線程已經cancel了,再執行get操作會拋出這個異常
Log.e(TAG, "future已經cancel了...");
e.printStackTrace();
}
2. Future + ExecutorService
//step1 封裝一個計算任務,實現Callable接口 如上
//step2:創建計算任務 如上
Task task = new Task();
//step3:創建線程池,將Callable類型的task提交給線程池執行,通過Future獲取子任務的執行結果
ExecutorService executorService = Executors.newCachedThreadPool();
final Future<Boolean> future = executorService.submit(task);
//step4:通過future獲取執行結果
boolean result = (boolean) future.get();
3. FutureTask + ExecutorService
//step1 封裝一個計算任務,實現Callable接口 如上
//step2:創建計算任務 如上
//step3:將FutureTask提交給線程池執行
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(futureTask);
//step4:通過future獲取執行結果
boolean result = (boolean) futureTask.get();
2. 開發可能出現的問題。
FutureTask使用還是比較簡單的,FutureTask與Runnable,最大的區別有兩個,一個是可以獲取執行結果,另一個是可以取消,使用方法可以參考以上步驟,不過使用FutureTask可能出現以下兩個問題:
- 有的情況下,使用 futuretask.cancel(true)方法並不能真正的結束子任務執行。
- FutureTask的get(long timeout, TimeUnit unit)方法,是等待timeout時間後,獲取子線程的執行結果,但是如果子任務執行結束了,但是超時時間還沒有到,這個方法也會返回結果。
3. 結合FutureTask的源碼分析問題。
下面,結合FutureTask的源碼,分析一下以上兩個問題。
成員變量
先看一下FutureTask內部比較值得關注的幾個成員變量。
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state;
/** The underlying callable; nulled out after running */
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
...
}
- private volatile int state,state用來標識當前任務的運行狀態。FutureTask的所有方法都是圍繞這個狀態進行的,需要注意,這個值用volatile(易變的)來標記,如果有多個子線程在執行FutureTask,那麼它們看到的都會是同一個state,有如下幾個值:
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;
NEW:表示這是一個新的任務,或者還沒有執行完的任務,是初始狀態。
COMPLETING:表示任務執行結束(正常執行結束,或者發生異常結束),但是還沒有將結果保存到outcome中。是一箇中間狀態。
NORMAL:表示任務正常執行結束,並且已經把執行結果保存到outcome字段中。是一個最終狀態。
EXCEPTIONAL:表示任務發生異常結束,異常信息已經保存到outcome中,這是一個最終狀態。
CANCELLED:任務在新建之後,執行結束之前被取消了,但是不要求中斷正在執行的線程,也就是調用了cancel(false),任務就是CANCELLED狀態,這時任務狀態變化是NEW -> CANCELLED。
INTERRUPTING:任務在新建之後,執行結束之前被取消了,並要求中斷線程的執行,也就是調用了cancel(true),這時任務狀態就是INTERRUPTING。這是一箇中間狀態。
INTERRUPTED:調用cancel(true)取消異步任務,會調用interrupt()中斷線程的執行,然後狀態會從INTERRUPTING變到INTERRUPTED。狀態變化有如下4種情況:
NEW -> COMPLETING -> NORMAL --------------------------------------- 正常執行結束的流程
NEW -> COMPLETING -> EXCEPTIONAL ---------------------執行過程中出現異常的流程
NEW -> CANCELLED -------------------------------------------被取消,即調用了cancel(false)
NEW -> INTERRUPTING -> INTERRUPTED -------------被中斷,即調用了cancel(true) -
private Callable<V> callable,一個Callable類型的變量,封裝了計算任務,可獲取計算結果。從上面的用法中可以看到,FutureTask的構造函數中,我們傳入的就是實現了Callable的接口的計算任務。
- private Object outcome,Object類型的變量outcome,用來保存計算任務的返回結果,或者執行過程中拋出的異常。
- private volatile Thread runner,指向當前在運行Callable任務的線程,runner在FutureTask中的賦值變化很值得關注,後面源碼會詳細介紹這個。
- private volatile WaitNode waiters,WaitNode是FutureTask的內部類,表示一個阻塞隊列,如果任務還沒有執行結束,那麼調用get()獲取結果的線程會阻塞,在這個阻塞隊列中排隊等待。
成員函數
下面從構造函數開始,看一下FutureTask的源碼。
1. 構造函數
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
FutureTask的第一個構造函數,參數是Callable類型的變量。將傳入的參數賦值給this.callable,然後設置state狀態爲NEW,表示這是新任務。
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
FutureTask還有一個構造函數,接收Runnable類型的參數,通過Executors.callable(runnable, result)將傳入的Runnable和result轉換成Callable類型。使用該構造方法,可以定製返回結果。
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
可以看一下Executors.callable(runnable, result)方法,這裏通過適配器模式進行適配,創建一個RunnableAdapter適配器。
private static final class RunnableAdapter<T> implements Callable<T> {
private final Runnable task;
private final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
RunnableAdapter是Executors的內部類,實現也比較簡單,實現了適配對象Callable接口,在call()方法中執行Runnable的run(),然後返回result。
2. 任務被執行——run()
FutureTask封裝了計算任務,無論是提交給Thread執行,或者線程池執行,調用的都是FutureTask的run()。
public void run() {
//1.判斷狀態是否是NEW,不是NEW,說明任務已經被其他線程執行,甚至執行結束,或者被取消了,直接返回
//2.調用CAS方法,判斷RUNNER爲null的話,就將當前線程保存到RUNNER中,設置RUNNER失敗,就直接返回
if (state != NEW ||
!U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//3.執行Callable任務,結果保存到result中
result = c.call();
ran = true;
} catch (Throwable ex) {
//3.1 如果執行任務過程中發生異常,將調用setException()設置異常
result = null;
ran = false;
setException(ex);
}
//3.2 任務正常執行結束調用set(result)保存結果
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
//4. 任務執行結束,runner設置爲null,表示當前沒有線程在執行這個任務了
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
//5. 讀取狀態,判斷是否在執行的過程中,被中斷了,如果被中斷,處理中斷
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
- 首先,判斷state的值是不是NEW,如果不是NEW,說明線程已經被執行了,可能已經執行結束,或者被取消了,直接返回。
- 這裏其實是調用了Unsafe的CAS方法,讀取並設置runner的值,將當前線程保存到runner中,表示當前正在執行任務的線程。可以看到,這裏設置的其實是RUNNER,和前面介紹的Thread類型的runner變量不一樣的,那爲什麼還說設置的是runner的值?RUNNER在FutureTask中定義如下:
private static final long RUNNER;//RUNNER是一個long類型的變量,指向runner字段的偏移地址,相當於指針 RUNNER = U.objectFieldOffset(FutureTask.class.getDeclaredField("runner"));
關於Unsafe的CAS方法,簡單介紹一下,它提供了一種對runner進行原子操作的方法,原子操作,意味着,這個操作不會被打斷。runner被volatile字段修飾,只能保證,當多個子線程在執行FutureTask的時候,它們讀取到的runner的值是同一個,但是不能保證原子操作,所以很容易讀到髒數據(舉個例子:線程A準備對runner進行讀和寫操作,讀取到runner的值爲null,這是,cpu切換執行線程B,線程B讀取到runner的值也是null,然後又切換到線程A執行,線程A對runner賦值thread-A,此時runner的值已經不再是null,線程B讀取到的runner=null就是髒數據),用Unsafe的CAS方法,來對runner進行讀寫,就能保證原子操作。多個線程訪問run()方法時,會在這裏同步。
-
讀取callable變量,執行call(),並獲取執行結果。
如果執行call()的過程中發生異常,就調用setException()設置異常,setException()定義如下:protected void setException(Throwable t) { if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) { outcome = t; U.putOrderedInt(this, STATE, EXCEPTIONAL); // final state finishCompletion(); } } //a. 調用Unsafe的CAS方法,state從NEW --> COMPLETING,這裏的STATE和上面的RUNNER定義類似,指向state字段的偏移地址。 //b. 將異常信息保存到outcome字段,state變成EXCEPTIONAL。 //c. 調用finishCompletion()。 //NEW --> COMPLETING --> EXCEPTIONAL。
- 任務執行結束,
runner
設置爲null,表示當前沒有線程在執行這個任務了。 - 讀取
state
狀態,判斷是否在執行的過程中被中斷了,如果被中斷,處理中斷,看一下這個中斷處理: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. if (s == INTERRUPTING) while (state == INTERRUPTING) Thread.yield(); // wait out pending interrupt }
如果狀態是
INTERRUPTING
,表示正在被中斷,這時就讓出線程的執行權,給其他線程來執行。
3. 獲取任務的執行結果——get()
一般情況下,執行任務的線程和獲取結果的線程不會是同一個,當我們在主線程或者其他線程中,獲取計算任務的結果時,就會調用get方法,如果這時計算任務還沒有執行完成,調用get()的線程就會阻塞等待。get()
實現如下:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
- 讀取任務的執行狀態 state ,如果
state <= COMPLETING
,說明線程還沒有執行完(run()中可以看到,只有任務執行結束,或者發生異常的時候,state纔會被設置成COMPLETING)。 - 調用
awaitDone(false, 0L)
,進入阻塞狀態。看一下awaitDone(false, 0L)
的實現:private int awaitDone(boolean timed, long nanos) throws InterruptedException { long startTime = 0L; // Special value 0L means not yet parked WaitNode q = null; boolean queued = false; for (;;) { //1. 讀取狀態 //1.1 如果s > COMPLETING,表示任務已經執行結束,或者發生異常結束了,就不會阻塞,直接返回 int s = state; if (s > COMPLETING) { if (q != null) q.thread = null; return s; } //1.2 如果s == COMPLETING,表示任務結束(正常/異常),但是結果還沒有保存到outcome字段,當前線程讓出執行權,給其他線程先執行 else if (s == COMPLETING) // We may have already promised (via isDone) that we are done // so never return empty-handed or throw InterruptedException Thread.yield(); //2. 如果調用get()的線程被中斷了,就從等待的線程棧中移除這個等待節點,然後拋出中斷異常 else if (Thread.interrupted()) { removeWaiter(q); throw new InterruptedException(); } //3. 如果等待節點q=null,就創建一個等待節點 else if (q == null) { if (timed && nanos <= 0L) return s; q = new WaitNode(); } //4. 如果這個等待節點還沒有加入等待隊列,就加入隊列頭 else if (!queued) queued = U.compareAndSwapObject(this, WAITERS, q.next = waiters, q); //5. 如果設置了超時等待時間 else if (timed) { //5.1 設置startTime,用於計算超時時間,如果超時時間到了,就等待隊列中移除當前節點 final long parkNanos; if (startTime == 0L) { // first time startTime = System.nanoTime(); if (startTime == 0L) startTime = 1L; parkNanos = nanos; } else { long elapsed = System.nanoTime() - startTime; if (elapsed >= nanos) { removeWaiter(q); return state; } parkNanos = nanos - elapsed; } // nanoTime may be slow; recheck before parking //5.2 如果超時時間還沒有到,而且任務還沒有結束,就阻塞特定時間 if (state < COMPLETING) LockSupport.parkNanos(this, parkNanos); } //6. 阻塞,等待喚醒 else LockSupport.park(this); } }
這裏主要有幾個步驟:
a. 讀取state,如果s > COMPLETING,表示任務已經執行結束,或者發生異常結束了,此時,調用get()的線程就不會阻塞;如果s == COMPLETING,表示任務結束(正常/異常),但是結果還沒有保存到outcome字段,當前線程讓出執行權,給其他線程先執行。
b. 判斷Thread.interrupted()
,如果調用get()
的線程被中斷了,就從等待的線程棧(其實就是一個WaitNode節點隊列或者說是棧)中移除這個等待節點,然後拋出中斷異常。
c. 判斷q == null,如果等待節點q爲null,就創建等待節點,這個節點後面會被插入阻塞隊列。
d. 判斷queued,這裏是將c中創建節點q加入隊列頭。使用Unsafe
的CAS方法,對waiters
進行賦值,waiters
也是一個WaitNode節點,相當於隊列頭,或者理解爲隊列的頭指針。通過WaitNode可以遍歷整個阻塞隊列。
e. 之後,判斷timed
,這是從get()
傳入的值,表示是否設置了超時時間。設置超時時間之後,調用get()
的線程最多阻塞nanos,就會從阻塞狀態醒過來。如果沒有設置超時時間,就直接進入阻塞狀態,等待被其他線程喚醒。awaitDone()
方法內部有一個無限循環,看似有很多判斷,比較難理解,其實這個循環最多循環3次。
假設Thread A執行了get()獲取計算任務執行結果,但是子任務還沒有執行完,而且Thread A沒有被中斷,它會進行以下步驟。
step1:Thread A執行了awaitDone()
,1,2兩次判斷都不成立,Thread A判斷q=null,會創建一個WaitNode節點q,然後進入第二次循環。
step2:第二次循環,判斷4不成立,此時將step1創建的節點q加入隊列頭。
step3:第三次循環,判斷是否設置了超時時間,如果設置了超時時間,就阻塞特定時間,否則,一直阻塞,等待被其他線程喚醒。 - 從
awaitDone()
返回,最後調用report(int s)
,這個後面再介紹。 -
取消任務——cancel(boolean mayInterruptIfRunning)
通常調用
cancel()
的線程和執行子任務的線程不會是同一個。當FutureTask
的cancel(boolean mayInterruptIfRunning)
方法被調用時,如果子任務還沒有執行,那麼這個任務就不會執行了,如果子任務已經執行,且mayInterruptIfRunning=true
,那麼執行子任務的線程會被中斷(注意:這裏說的是線程被中斷,不是任務被取消),下面看一下這個方法的實現:public boolean cancel(boolean mayInterruptIfRunning) { //1.判斷state是否爲NEW,如果不是NEW,說明任務已經結束或者被取消了,該方法會執行返回false //state=NEW時,判斷mayInterruptIfRunning,如果mayInterruptIfRunning=true,說明要中斷任務的執行,NEW->INTERRUPTING //如果mayInterruptIfRunning=false,不需要中斷,狀態改爲CANCELLED if (!(state == NEW && U.compareAndSwapInt(this, STATE, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; try { // in case call to interrupt throws exception if (mayInterruptIfRunning) { try { //2.讀取當前正在執行子任務的線程runner,調用t.interrupt(),中斷線程執行 Thread t = runner; if (t != null) t.interrupt(); } finally { // final state //3.修改狀態爲INTERRUPTED U.putOrderedInt(this, STATE, INTERRUPTED); } } } finally { finishCompletion(); } return true; }
cancel()分析:
- 判斷state,保證state = NEW才能繼續cancel()的後續操作。state=NEW且mayInterruptIfRunning=true,說明要中斷任務的執行,此時,NEW->INTERRUPTING。然後讀取當前執行任務的線程runner,調用t.interrupt(),中斷線程執行,NEW->INTERRUPTING->INTERRUPTED,最後調用finishCompletion()。
- 如果NEW->INTERRUPTING,那麼cancel()方法,只是修改了狀態,NEW->CANCELLED,然後直接調用finishCompletion()。
- 所以cancel(true)方法,只是調用t.interrupt(),此時,如果t因爲sleep(),wait()等方法進入阻塞狀態,那麼阻塞的地方會拋出InterruptedException;如果線程正常運行,需要結合Thread的interrupted()方法進行判斷,才能結束,否則,cancel(true)不能結束正在執行的任務。
這也就可以解釋,有的情況下,使用 futuretask.cancel(true)方法並不能真正的結束子任務執行。
-
子線程返回結果前的最後一步——finishCompletion()
前面多次出現過這個方法,set(V v)(保存執行結果,設置狀態爲NORMAL),setException(Throwable t)(保存結果,設置狀態爲EXCEPTIONAL)和cancel(boolean mayInterruptIfRunning)(設置狀態爲CANCELLED/INTERRUPTED),該方法在state變成最終態之後,會被調用。
private void finishCompletion() { // assert state > COMPLETING; for (WaitNode q; (q = waiters) != null;) { if (U.compareAndSwapObject(this, WAITERS, 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 }
finishCompletion()主要做了三件事情:
- 遍歷waiters等待隊列,調用LockSupport.unpark(t)喚醒等待返回結果的線程,釋放資源。
- 調用done(),這個方法什麼都沒有做,不過子類可以實現這個方法,做一些額外的操作。
- 設置callable爲null,callable是FutureTask封裝的任務,任務執行完,釋放資源。
這裏可以解答上面的第二個問題了。FutureTask的get(long timeout, TimeUnit unit)方法,表示阻塞timeout時間後,獲取子線程的執行結果,但是如果子任務執行結束了,但是超時時間還沒有到,這個方法也會返回結果。因爲任務執行完之後,會遍歷阻塞隊列,喚醒阻塞的線程。LockSupport.unpark(t)執行之後,阻塞的線程會從LockSupport.park(this)/LockSupport.parkNanos(this, parkNanos)醒來,然後會繼續進入awaitDone(boolean timed, long nanos)的while循環,此時,state >= COMPLETING,然後從awaitDone()返回。此時,get()/get(long timeout, TimeUnit unit)會繼續執行,return report(s),上面介紹get()的時候沒介紹的方法。看一下report(int s):
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);
}
其實就是讀取outcome,將state映射到最後返回的結果中,s == NORMAL說明任務正常結束,返回正常結果,s >= CANCELLED,就拋出CancellationException。
這裏補充一下:
LockSupport.parkNanos與Thread.Sleep的確具有相似的語義。二者的最終都是根據條件變量超時等待函數。LockSupport.parkNanos():爲了線程調度,在指定的時限前禁用當前線程,除非許可可用。是可以在阻塞時間沒有到的時候,通過unpark來喚醒,提前跳出阻塞。
Sleep()是單純的在指定時間內讓當前正在執行的線程暫停執行,但不會釋放“鎖標誌”,使當前線程進入阻塞狀態,在指定時間內不會執行。
6.其他方法
FutureTask的還有兩個方法isCancelled()和isDone(),其實就是判斷state,沒有過多的步驟。
public boolean isCancelled() {
return state >= CANCELLED;
}
public boolean isDone() {
return state != NEW;
}
4. 精華部分
LockSupport中的park() 和 unpark() 的作用分別是阻塞線程和解除阻塞線程,其使用方式:簡單,靈活
簡單
這裏先通過對比來介紹LockSupport。在沒有LockSupport之前,線程的掛起和喚醒咱們都是通過Object的wait和notify/notifyAll方法實現。
寫一段例子代碼,線程A執行一段業務邏輯後調用wait阻塞住自己。主線程調用notify方法喚醒線程A,線程A然後打印自己執行的結果。
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
obj.wait();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
Thread.sleep(1000);
obj.notify();
}
}
執行這段代碼,不難發現這個錯誤:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
原因很簡單,wait和notify/notifyAll方法只能在同步代碼塊裏用。所以將代碼修改爲如下就可正常運行了:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj){
obj.notify();
}
}
}
那如果換成LockSupport呢?簡單得很,看代碼:
public class TestObjWait {
public static void main(String[] args)throws Exception {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
}
});
A.start();
//睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(A);
}
}
直接調用就可以,沒有說非得在同步代碼塊裏才能用。
靈活
先看一個例子:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
//Thread.sleep(1000);
synchronized (obj){
obj.notify();
}
}
}
多運行幾次上邊的代碼,有的時候能夠正常打印結果並退出程序,但有的時候線程無法打印結果阻塞住了。原因就在於:主線程調用完notify後,線程A才進入wait方法,導致線程A一直阻塞住。由於線程A不是後臺線程,所以整個程序無法退出。
那如果換做LockSupport呢?LockSupport就支持主線程先調用unpark後,線程A再調用park而不被阻塞嗎?是的,沒錯。代碼如下:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
}
});
A.start();
//睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
//Thread.sleep(1000);
LockSupport.unpark(A);
}
}
不管你執行多少次,這段代碼都能正常打印結果並退出。這就是LockSupport最大的靈活所在。
總結一下,LockSupport比Object的wait/notify有三大優勢:
- LockSupport不需要在同步代碼塊裏 。所以線程間也不需要維護一個共享的同步對象了,實現了線程間的解耦。
- unpark函數可以先於park調用,因爲這個特點,我們可以不用擔心掛起和恢復時序問題。。
- 結合1和2,得到的第三點優勢:使用比較不容易出錯,對線程阻塞與喚醒時可以優先選擇這種方式而不是object的wait和notify的方式。
LockSupport的實現
學習要知其然,還要知其所以然。下面看看LockSupport的實現:
進入LockSupport的park方法,可以發現它是調用了Unsafe的park方法,這是一個本地native方法,只能通過openjdk的源碼看看其本地實現了。
它調用了線程的Parker類型對象的park方法,如下是Parker類的定義:
類中定義了一個int類型的_counter變量,咱們上文中講靈活性的那一節說,可以先執行unpark後執行park,就是通過這個變量實現,看park方法的實現代碼(由於方法比較長就不整體截圖了):
park方法會調用Atomic::xchg方法,這個方法會原子性的將_counter賦值爲0,並返回賦值前的值。如果調用park方法前,_counter大於0,則說明之前調用過unpark方法,所以park方法直接返回。
接着往下看:
實際上Parker類用Posix的mutex,condition來實現的阻塞喚醒。如果對mutex和condition不熟,可以簡單理解爲mutex就是Java裏的synchronized,condition就是Object裏的wait/notify操作。
park方法裏調用pthread_mutex_trylock方法,就相當於Java線程進入Java的同步代碼塊,然後再次判斷_counter是否大於零,如果大於零則將_counter設置爲零。最後調用pthread_mutex_unlock解鎖,相當於Java執行完退出同步代碼塊。如果_counter不大於零,則繼續往下執行pthread_cond_wait方法,實現當前線程的阻塞。
最後再看看unpark方法的實現吧,這塊就簡單多了,直接上代碼:
圖中的1和4就相當於Java的進入synchronized和退出synchronized的加鎖解鎖操作,代碼2將_counter設置爲1,同時判斷先前_counter的值是否小於1,即這段代碼:if(s<1)。如果不小於1,則就不會有線程被park,所以方法直接執行完畢,否則就會執行代碼3,來喚醒被阻塞的線程。
通過閱讀LockSupport的本地實現,我們不難發現這麼個問題:多次調用unpark方法和調用一次unpark方法效果一樣,因爲都是直接將_counter賦值爲1,而不是加1。簡單說就是:線程A連續調用兩次LockSupport.unpark(B)方法喚醒線程B,然後線程B調用兩次LockSupport.park()方法, 線程B依舊會被阻塞。因爲兩次unpark調用效果跟一次調用一樣,只能讓線程B的第一次調用park方法不被阻塞,第二次調用依舊會阻塞。
總結
到此FutureTask分析完畢,其中感受最深的是Unsafe
的用法,對於多線程共享的對象,採用volatile
+ Unsafe
的方法,代替鎖操作,進行同步;其次,是LockSupport
的park(Object blocker)
和unpark(Thread thread)
的使用
park(Object blocker)
:線程進入阻塞狀態,告訴線程調度,當前線程不可用,直到線程再次獲取permit
(允許);如果在調用park(Object blocker)
之前,線程已經獲得了permit
(比如說,已經調用了unpark(t)),那麼該方法會返回。unpark(Thread thread)
:使得傳入的線程再次獲得permit
.這裏的permit
可以理解爲一個信號量,多次調用和一次調用的效果一致。
參考:
1.https://www.jianshu.com/p/55221d045f39