FutureTask為何單個任務僅執(zhí)行一次原理解析
引言
前幾天會員領(lǐng)取情況查詢的接口SQL查詢超時出故障了,因為有個用戶買的會員有點多(哈哈),其實是 數(shù)據(jù)量大 + 祖?zhèn)鞔a邏輯冗長
嘗試的解決方案:
SQL:檢查了一下,單個SQL的耗時其實不算大,也能接受,不需要改動,主要原因是后端邏輯冗長
FutureTask獲取線程的執(zhí)行結(jié)果:將1次大查詢劃分為多次小查詢同時進行,提高接口響應速度。且一個FutureTask僅執(zhí)行一次,不會出現(xiàn)重復的查詢
經(jīng)過權(quán)衡,我們選擇了后者
一、FutureTask用法
解決方案要用到線程池搭配FutureTask,這里我們就不用了,簡化點
public class Test {
//計算結(jié)果
int count=0;
@Test
public void test(){
try{
FutureTask<Integer> futureTask=new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1;
}
});
//把FutureTask放入線程中,線程會運行FutureTask的run()代碼塊
Thread t1=new Thread(futureTask);
t1.start();
//獲取計算的結(jié)果,是一個阻塞等待返回的方法
count+=futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//最后結(jié)果: 1
System.out.println(count);
}
}這里用了構(gòu)造方法public FutureTask(Callable<V> callable)讓FutureTask持有Callable接口的實例
用到try-catch是由于futureTask.get()方法是一個阻塞等待的過程,途中如果被中斷會拋中斷異常,別的異常都會以ExecutionException執(zhí)行異常的形式拋出
二、(重要)FutureTask的任務僅執(zhí)行一次,為何?
FutureTask的run()代碼塊僅執(zhí)行一次!請看注釋
/**
執(zhí)行結(jié)果(全局變量), 有2種情況:
1. 順利完成返回的結(jié)果
2. 執(zhí)行run()代碼塊過程中拋出的異常
*/
private Object outcome;
//正在執(zhí)行run()的線程, 內(nèi)存可被其他線程可見
private volatile Thread runner;
???????public void run() {
/**
FutureTask的run()僅執(zhí)行一次的原因:
1. state != NEW表示任務正在被執(zhí)行或已經(jīng)完成, 直接return
2. 若state==NEW, 則嘗試CAS將當前線程 設置為執(zhí)行run()的線程,如果失敗,說明已經(jīng)有其他線程 先行一步執(zhí)行了run(),則當前線程return退出
*/
if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
return;
try {
//持有Callable的實例,后續(xù)會執(zhí)行該實例的call()方法
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;
//執(zhí)行中拋的異常會放入outcome中保存
setException(ex);
}
if (ran)
//若無異常, 順利完成的執(zhí)行結(jié)果會放入outcome保存
set(result);
}
}finally {
// help GC
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}執(zhí)行run()的代碼塊之后,其他線程如何拿到FutureTask的執(zhí)行結(jié)果?下面的get()方法可以做到
三、get()獲取結(jié)果
public V get() throws InterruptedException, ExecutionException {
int s = state;
//COMPLETING: 正在完成的狀態(tài); s <= COMPLETING就是未完成
if (s <= COMPLETING)
//不計時等待,結(jié)束等待的條件只有【完成】、【被中斷】、【被取消】、【拋其他異常(不包括中斷異常、取消異常)】
s = awaitDone(false, 0L);
return report(s);
}這里提一下線程執(zhí)行的狀態(tài) :
private volatile int state; //線程創(chuàng)建狀態(tài) private static final int NEW = 0; //完成(**一個瞬時的標記**) private static final int COMPLETING = 1; //正常完成狀態(tài) private static final int NORMAL = 2; //執(zhí)行過程出現(xiàn)異常 private static final int EXCEPTIONAL = 3; //執(zhí)行過程中被取消 private static final int CANCELLED = 4; //線程執(zhí)行被中斷(**一個瞬時的標記**) private static final int INTERRUPTING = 5; //線程執(zhí)行被中斷的狀態(tài) private static final int INTERRUPTED = 6;
volatile保證了線程執(zhí)行的狀態(tài)改變之后會刷新到內(nèi)存中,被其他線程可見
如果線程還處于未完成的狀態(tài),即s <= COMPLETING,就會進入等待狀態(tài),調(diào)用awaitDone(false, 0L)方法
get為何阻塞等待?
/**
@param timed 若是true則為定時等待,超時后會結(jié)束等待,并返回當前狀態(tài)state
@param nanos 如果是定時等待即第一個入?yún)imed=true的話,會設置對應的等待時長
*/
private int awaitDone(boolean timed, long nanos) throws InterruptedException {
//等待的最后期限
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
//進入無限循環(huán)的等待狀態(tài),只有【完成】、【被取消】、【異?!?、【中斷】、【超時】這五種情況才會結(jié)束等待
for (;;) {
if (Thread.interrupted()) {
//線程執(zhí)行被中斷,則移除等待結(jié)點并拋出異常
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
//【完成】、【被取消】、【拋其他異常】的狀態(tài)都會 在這 結(jié)束等待
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//子線程處于任務完成的瞬時狀態(tài),要等一會才能拿到執(zhí)行結(jié)果
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) {
//設置定時等待并且已經(jīng)超時了
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}詳細的注釋在代碼中,請耐心看一下。
簡單來說,能結(jié)束等待的條件只有5個:
- 完成
- 被中斷
- 設置定時等待并超時
- 被取消
- 拋了其他異常,比如RuntimeException,這里的其他異常既不是中斷異常,也不是取消異常
調(diào)用futureTask.get()的等待方式有2種,分為定時等待和 不計時等待:
- timed=true是定時等待,會創(chuàng)建等待結(jié)點q = new WaitNode();并放在棧頂(隊列頭部),然后掛起。結(jié)束等待的條件(滿足任一即可)是【完成】、【被中斷】、【被取消】、【拋其他異?!俊ⅰ境瑫r】 。
- timed=false是不計時等待,創(chuàng)建等待結(jié)點后會一直掛起,只有【完成】、【被中斷】、【被取消】、【拋其他異?!?/li>
在等待結(jié)束之前,LockSupport.park(this);表示線程會被一直掛起,不再繼續(xù)無限循環(huán)占用CPU。
解除掛起的條件是state > COMPLETING,然后調(diào)用finishCompletion()方法去讓線程解除掛起并回到awaitDone()做最后一次循環(huán)后return state
從get中返回結(jié)果report(int s)
/*正常的計算結(jié)果 or 拋出的異常 都會作為outcome*/
private Object outcome;
private V report(int s) throws ExecutionException {
Object x = outcome;
//正常完成
if (s == NORMAL)
return (V)x;
//執(zhí)行的過程中【被取消】
if (s >= CANCELLED)
throw new CancellationException();
/**
這里拋的是執(zhí)行過程中發(fā)生的其他異常,既不是【中斷異?!?也不是【被取消異?!?
比如發(fā)生了RuntimeException之類的就會在這拋
*/
throw new ExecutionException((Throwable)x);
}report(int s)是執(zhí)行g(shù)et()獲取結(jié)果的最后一步
看到這可能有朋友暈了,我把get()內(nèi)部的流程梳理一下:
若要等待計算結(jié)果:get() -> awaitDone() -> report(),共3步
不用等待:get() -> report() ,僅2步
四、FutureTask是如何拿到線程執(zhí)行的結(jié)果?
主要 有賴于FutureTask類內(nèi)部的Callable接口
只有Callable接口能拿到線程的返回值,下面來看下FutureTask的構(gòu)造函數(shù)
public class FutureTask<V> implements RunnableFuture<V> {
//執(zhí)行任務并返回結(jié)果
private Callable<V> callable;
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
//新建狀態(tài)
this.state = NEW;
}
}其實Callable 接口是沒法 作為創(chuàng)建線程new Thread(Runnable target)的入?yún)⒌?,只有借助FutureTask類才能被線程執(zhí)行,因為FutureTask實現(xiàn)了Runnable 接口
有興趣的可以看一下Future接口的關(guān)系圖(這里拿了大佬的圖,侵刪)

FutureTask類最終實現(xiàn)了Future接口和Runnable接口,可作為new Thread(Runnable target)的入?yún)arget來創(chuàng)建線程
五、FutureTask可能的執(zhí)行過程
順利完成 :NEW -> COMPLETING -> NORMAL ,即新建->正在完成 ->正常
NEW -> COMPLETING -> EXCEPTIONAL, 執(zhí)行過程出現(xiàn)了異常
被取消:NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED,新建 ->正在被中斷 ->中斷完成
六、列舉一下FutureTask的特性和應用場景
特性:
- 異步執(zhí)行,可執(zhí)行多次(通過runAndReset()方法),也可僅執(zhí)行一次(執(zhí)行run()即可)
- 可獲取線程執(zhí)行結(jié)果
應用場景:
- 長時間運行的任務,包含遠程調(diào)用的任務
- 數(shù)據(jù)量大的查詢,劃分為多個小查詢,每個FutureTask 僅執(zhí)行一次 的特性能有效避免重復的查詢
- 計算密集型的任務
以上就是FutureTask為何單個任務僅執(zhí)行一次原理解析的詳細內(nèi)容,更多關(guān)于FutureTask單任務執(zhí)行一次的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot2.x 使用 Log4j2 異步打印日志的實現(xiàn)
這篇文章主要介紹了Springboot2.x 使用 Log4j2 異步打印日志的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12
深入解讀Java代碼組織中的package包結(jié)構(gòu)
這篇文章主要介紹了Java代碼組織中的package包結(jié)構(gòu),是Java入門學習中的基礎知識,需要的朋友可以參考下2016-03-03
Java ArrayList與Vector和LinkedList的使用及源碼分析
ArrayList、Vector、LinkedList類均在java.util包中,均為可伸縮數(shù)組,即可以動態(tài)改變長度的數(shù)組。ArrayList 和 Vector都是基于存儲元素的Object[] array來實現(xiàn)的,它們會在內(nèi)存中開辟一塊連續(xù)的內(nèi)存來存儲2022-11-11
maven中自定義MavenArchetype的實現(xiàn)
Maven自身提供了許多Archetype來方便用戶創(chuàng)建Project,為了避免在創(chuàng)建project時重復的拷貝和修改,我們通過自定義Archetype來規(guī)范顯得還蠻有必要,下面就來介紹一下,感興趣的可以了解一下2025-01-01

