FutureTask為何單個(gè)任務(wù)僅執(zhí)行一次原理解析
引言
前幾天會(huì)員領(lǐng)取情況查詢的接口SQL查詢超時(shí)出故障了,因?yàn)橛袀€(gè)用戶買(mǎi)的會(huì)員有點(diǎn)多(哈哈),其實(shí)是 數(shù)據(jù)量大 + 祖?zhèn)鞔a邏輯冗長(zhǎng)
嘗試的解決方案:
SQL:檢查了一下,單個(gè)SQL的耗時(shí)其實(shí)不算大,也能接受,不需要改動(dòng),主要原因是后端邏輯冗長(zhǎng)
FutureTask獲取線程的執(zhí)行結(jié)果:將1次大查詢劃分為多次小查詢同時(shí)進(jìn)行,提高接口響應(yīng)速度。且一個(gè)FutureTask僅執(zhí)行一次,不會(huì)出現(xiàn)重復(fù)的查詢
經(jīng)過(guò)權(quán)衡,我們選擇了后者
一、FutureTask用法
解決方案要用到線程池搭配FutureTask,這里我們就不用了,簡(jiǎn)化點(diǎn)
public class Test { //計(jì)算結(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放入線程中,線程會(huì)運(yùn)行FutureTask的run()代碼塊 Thread t1=new Thread(futureTask); t1.start(); //獲取計(jì)算的結(jié)果,是一個(gè)阻塞等待返回的方法 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接口的實(shí)例
用到try-catch是由于futureTask.get()方法是一個(gè)阻塞等待的過(guò)程,途中如果被中斷會(huì)拋中斷異常,別的異常都會(huì)以ExecutionException執(zhí)行異常的形式拋出
二、(重要)FutureTask的任務(wù)僅執(zhí)行一次,為何?
FutureTask的run()代碼塊僅執(zhí)行一次!請(qǐng)看注釋
/** 執(zhí)行結(jié)果(全局變量), 有2種情況: 1. 順利完成返回的結(jié)果 2. 執(zhí)行run()代碼塊過(guò)程中拋出的異常 */ private Object outcome; //正在執(zhí)行run()的線程, 內(nèi)存可被其他線程可見(jiàn) private volatile Thread runner; ???????public void run() { /** FutureTask的run()僅執(zhí)行一次的原因: 1. state != NEW表示任務(wù)正在被執(zhí)行或已經(jīng)完成, 直接return 2. 若state==NEW, 則嘗試CAS將當(dāng)前線程 設(shè)置為執(zhí)行run()的線程,如果失敗,說(shuō)明已經(jīng)有其他線程 先行一步執(zhí)行了run(),則當(dāng)前線程return退出 */ if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread())) return; try { //持有Callable的實(shí)例,后續(xù)會(huì)執(zhí)行該實(shí)例的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í)行中拋的異常會(huì)放入outcome中保存 setException(ex); } if (ran) //若無(wú)異常, 順利完成的執(zhí)行結(jié)果會(huì)放入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) //不計(jì)時(shí)等待,結(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; //完成(**一個(gè)瞬時(shí)的標(biāo)記**) private static final int COMPLETING = 1; //正常完成狀態(tài) private static final int NORMAL = 2; //執(zhí)行過(guò)程出現(xiàn)異常 private static final int EXCEPTIONAL = 3; //執(zhí)行過(guò)程中被取消 private static final int CANCELLED = 4; //線程執(zhí)行被中斷(**一個(gè)瞬時(shí)的標(biāo)記**) private static final int INTERRUPTING = 5; //線程執(zhí)行被中斷的狀態(tài) private static final int INTERRUPTED = 6;
volatile保證了線程執(zhí)行的狀態(tài)改變之后會(huì)刷新到內(nèi)存中,被其他線程可見(jiàn)
如果線程還處于未完成的狀態(tài),即s <= COMPLETING,就會(huì)進(jìn)入等待狀態(tài),調(diào)用awaitDone(false, 0L)方法
get為何阻塞等待?
/** @param timed 若是true則為定時(shí)等待,超時(shí)后會(huì)結(jié)束等待,并返回當(dāng)前狀態(tài)state @param nanos 如果是定時(shí)等待即第一個(gè)入?yún)imed=true的話,會(huì)設(shè)置對(duì)應(yīng)的等待時(shí)長(zhǎng) */ private int awaitDone(boolean timed, long nanos) throws InterruptedException { //等待的最后期限 final long deadline = timed ? System.nanoTime() + nanos : 0L; WaitNode q = null; boolean queued = false; //進(jìn)入無(wú)限循環(huán)的等待狀態(tài),只有【完成】、【被取消】、【異常】、【中斷】、【超時(shí)】這五種情況才會(huì)結(jié)束等待 for (;;) { if (Thread.interrupted()) { //線程執(zhí)行被中斷,則移除等待結(jié)點(diǎn)并拋出異常 removeWaiter(q); throw new InterruptedException(); } int s = state; //【完成】、【被取消】、【拋其他異?!康臓顟B(tài)都會(huì) 在這 結(jié)束等待 if (s > COMPLETING) { if (q != null) q.thread = null; return s; } //子線程處于任務(wù)完成的瞬時(shí)狀態(tài),要等一會(huì)才能拿到執(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) { //設(shè)置定時(shí)等待并且已經(jīng)超時(shí)了 nanos = deadline - System.nanoTime(); if (nanos <= 0L) { removeWaiter(q); return state; } LockSupport.parkNanos(this, nanos); } else LockSupport.park(this); } }
詳細(xì)的注釋在代碼中,請(qǐng)耐心看一下。
簡(jiǎn)單來(lái)說(shuō),能結(jié)束等待的條件只有5個(gè):
- 完成
- 被中斷
- 設(shè)置定時(shí)等待并超時(shí)
- 被取消
- 拋了其他異常,比如RuntimeException,這里的其他異常既不是中斷異常,也不是取消異常
調(diào)用futureTask.get()的等待方式有2種,分為定時(shí)等待和 不計(jì)時(shí)等待:
- timed=true是定時(shí)等待,會(huì)創(chuàng)建等待結(jié)點(diǎn)q = new WaitNode();并放在棧頂(隊(duì)列頭部),然后掛起。結(jié)束等待的條件(滿足任一即可)是【完成】、【被中斷】、【被取消】、【拋其他異?!?、【超時(shí)】 。
- timed=false是不計(jì)時(shí)等待,創(chuàng)建等待結(jié)點(diǎn)后會(huì)一直掛起,只有【完成】、【被中斷】、【被取消】、【拋其他異?!?/li>
在等待結(jié)束之前,LockSupport.park(this);表示線程會(huì)被一直掛起,不再繼續(xù)無(wú)限循環(huán)占用CPU。
解除掛起的條件是state > COMPLETING,然后調(diào)用finishCompletion()方法去讓線程解除掛起并回到awaitDone()做最后一次循環(huán)后return state
從get中返回結(jié)果report(int s)
/*正常的計(jì)算結(jié)果 or 拋出的異常 都會(huì)作為outcome*/ private Object outcome; private V report(int s) throws ExecutionException { Object x = outcome; //正常完成 if (s == NORMAL) return (V)x; //執(zhí)行的過(guò)程中【被取消】 if (s >= CANCELLED) throw new CancellationException(); /** 這里拋的是執(zhí)行過(guò)程中發(fā)生的其他異常,既不是【中斷異?!?也不是【被取消異常】 比如發(fā)生了RuntimeException之類(lèi)的就會(huì)在這拋 */ throw new ExecutionException((Throwable)x); }
report(int s)是執(zhí)行g(shù)et()獲取結(jié)果的最后一步
看到這可能有朋友暈了,我把get()內(nèi)部的流程梳理一下:
若要等待計(jì)算結(jié)果:get() -> awaitDone() -> report(),共3步
不用等待:get() -> report() ,僅2步
四、FutureTask是如何拿到線程執(zhí)行的結(jié)果?
主要 有賴于FutureTask類(lèi)內(nèi)部的Callable接口
只有Callable接口能拿到線程的返回值,下面來(lái)看下FutureTask的構(gòu)造函數(shù)
public class FutureTask<V> implements RunnableFuture<V> { //執(zhí)行任務(wù)并返回結(jié)果 private Callable<V> callable; public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; //新建狀態(tài) this.state = NEW; } }
其實(shí)Callable 接口是沒(méi)法 作為創(chuàng)建線程new Thread(Runnable target)的入?yún)⒌?,只有借助FutureTask類(lèi)才能被線程執(zhí)行,因?yàn)镕utureTask實(shí)現(xiàn)了Runnable 接口
有興趣的可以看一下Future接口的關(guān)系圖(這里拿了大佬的圖,侵刪)
FutureTask類(lèi)最終實(shí)現(xiàn)了Future接口和Runnable接口,可作為new Thread(Runnable target)的入?yún)arget來(lái)創(chuàng)建線程
五、FutureTask可能的執(zhí)行過(guò)程
順利完成 :NEW -> COMPLETING -> NORMAL ,即新建->正在完成 ->正常
NEW -> COMPLETING -> EXCEPTIONAL, 執(zhí)行過(guò)程出現(xiàn)了異常
被取消:NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED,新建 ->正在被中斷 ->中斷完成
六、列舉一下FutureTask的特性和應(yīng)用場(chǎng)景
特性:
- 異步執(zhí)行,可執(zhí)行多次(通過(guò)runAndReset()方法),也可僅執(zhí)行一次(執(zhí)行run()即可)
- 可獲取線程執(zhí)行結(jié)果
應(yīng)用場(chǎng)景:
- 長(zhǎng)時(shí)間運(yùn)行的任務(wù),包含遠(yuǎn)程調(diào)用的任務(wù)
- 數(shù)據(jù)量大的查詢,劃分為多個(gè)小查詢,每個(gè)FutureTask 僅執(zhí)行一次 的特性能有效避免重復(fù)的查詢
- 計(jì)算密集型的任務(wù)
以上就是FutureTask為何單個(gè)任務(wù)僅執(zhí)行一次原理解析的詳細(xì)內(nèi)容,更多關(guān)于FutureTask單任務(wù)執(zhí)行一次的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于Spring Boot動(dòng)態(tài)權(quán)限變更問(wèn)題的實(shí)現(xiàn)方案
這篇文章主要介紹了Spring Boot動(dòng)態(tài)權(quán)限變更實(shí)現(xiàn)的整體方案使用session作為緩存,結(jié)合AOP技術(shù)進(jìn)行token認(rèn)證和權(quán)限控制,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06springBoot 整合ModBus TCP的詳細(xì)過(guò)程
ModBus是一種串行通信協(xié)議,用于從儀器和控制設(shè)備傳輸信號(hào)到主控制器或數(shù)據(jù)采集系統(tǒng),它分為主站和從站,主站獲取和編寫(xiě)數(shù)據(jù),從站則是設(shè)備,本文給大家介紹springBoot 整合ModBus TCP的詳細(xì)過(guò)程,感興趣的朋友一起看看吧2025-01-01分析講解Java?Random類(lèi)里的種子問(wèn)題
Random類(lèi)中實(shí)現(xiàn)的隨機(jī)算法是偽隨機(jī),也就是有規(guī)則的隨機(jī)。在進(jìn)行隨機(jī)時(shí),隨機(jī)算法的起源數(shù)字稱為種子數(shù)(seed),在種子數(shù)的基礎(chǔ)上進(jìn)行一定的變換,從而產(chǎn)生需要的隨機(jī)數(shù)字2022-05-05idea創(chuàng)建SpringBoot項(xiàng)目及注解配置相關(guān)應(yīng)用小結(jié)
Spring Boot是Spring社區(qū)發(fā)布的一個(gè)開(kāi)源項(xiàng)目,旨在幫助開(kāi)發(fā)者快速并且更簡(jiǎn)單的構(gòu)建項(xiàng)目,Spring Boot框架,其功能非常簡(jiǎn)單,便是幫助我們實(shí)現(xiàn)自動(dòng)配置,本文給大家介紹idea創(chuàng)建SpringBoot項(xiàng)目及注解配置相關(guān)應(yīng)用,感興趣的朋友跟隨小編一起看看吧2023-11-11深入理解Java中的final關(guān)鍵字_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Java中的final關(guān)鍵字非常重要,它可以應(yīng)用于類(lèi)、方法以及變量。這篇文章中我將帶你看看什么是final關(guān)鍵字以及使用final的好處,具體內(nèi)容詳情通過(guò)本文學(xué)習(xí)吧2017-04-04一篇文章帶你了解mybatis的動(dòng)態(tài)SQL
這篇文章主要為大家介紹了mybatis的動(dòng)態(tài)SQL?,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-01-01如何對(duì)quartz定時(shí)任務(wù)設(shè)置結(jié)束時(shí)間
這篇文章主要介紹了如何對(duì)quartz定時(shí)任務(wù)設(shè)置結(jié)束時(shí)間問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12Java?常量池詳解之class文件常量池?和class運(yùn)行時(shí)常量池
這篇文章主要介紹了Java?常量池詳解之class文件常量池?和class運(yùn)行時(shí)常量池,常量池主要存放兩大類(lèi)常量:字面量,符號(hào)引用,本文結(jié)合示例代碼對(duì)java class常量池相關(guān)知識(shí)介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12