好記性不如爛筆頭普泡,記錄下來的才是永恒播掷!這里是JavaQ大本營,誠邀關(guān)注撼班。
上篇《這樣的API網(wǎng)關(guān)查詢接口優(yōu)化歧匈,我是被迫的》文章末尾,有朋友留言提到文中的場景是IO密集型操作砰嘁,不是CPU密集操作件炉,不需要使用線程池勘究,我猜這位朋友可能想表達(dá)的是IO密集且阻塞時間久的不要使用線程池方案解決。IO密集型在控制好同步處理時間或阻塞等待的條件下是可以使用線程池的斟冕,不知道這么描述是否合理口糕,有高見的大佬可以繼續(xù)留言討論昌跌。
關(guān)注過我更新頻率的朋友會發(fā)現(xiàn)有好幾天沒有上新內(nèi)容了宝剖,原因有二握联,一是最近真的太忙了篡悟,項目催的緊,程序員哪有不加班是吧斟赚;另一個是我正在梳理技能圖譜镐侯,后續(xù)的內(nèi)容更新會根據(jù)這個圖譜來搀继,還在進(jìn)行中呵燕,有興趣的朋友持續(xù)關(guān)注下我和我的github:https://github.com/wind7rui/JavaHub棠绘,持續(xù)更新哦!好了再扭,開始我們今天的話題~線程池氧苍。注意:下方多圖高能預(yù)警,建議先收藏后閱讀霍衫,防止走丟!
為什么要使用線程池
平時討論多線程處理侯养,大佬們必定會說使用線程池敦跌,那為什么要使用線程池?其實逛揩,這個問題可以反過來思考一下柠傍,不使用線程池會怎么樣?當(dāng)需要多線程并發(fā)執(zhí)行任務(wù)時辩稽,只能不斷的通過new Thread創(chuàng)建線程惧笛,每創(chuàng)建一個線程都需要在堆上分配內(nèi)存空間,同時需要分配虛擬機(jī)棧逞泄、本地方法棧患整、程序計數(shù)器等線程私有的內(nèi)存空間,當(dāng)這個線程對象被可達(dá)性分析算法標(biāo)記為不可用時被GC回收喷众,這樣頻繁的創(chuàng)建和回收需要大量的額外開銷各谚。再者說,JVM的內(nèi)存資源是有限的到千,如果系統(tǒng)中大量的創(chuàng)建線程對象昌渤,JVM很可能直接拋出OutOfMemoryError異常,還有大量的線程去競爭CPU會產(chǎn)生其他的性能開銷憔四,更多的線程反而會降低性能膀息,所以必須要限制線程數(shù)般眉。
既然不使用線程池有那么多問題,我們來看一下使用線程池有哪些好處:
使用線程池可以復(fù)用池中的線程潜支,不需要每次都創(chuàng)建新線程甸赃,減少創(chuàng)建和銷毀線程的開銷;
同時毁腿,線程池具有隊列緩沖策略辑奈、拒絕機(jī)制和動態(tài)管理線程個數(shù),特定的線程池還具有定時執(zhí)行已烤、周期執(zhí)行功能鸠窗,比較重要的一點是線程池可實現(xiàn)線程環(huán)境的隔離,例如分別定義支付功能相關(guān)線程池和優(yōu)惠券功能相關(guān)線程池胯究,當(dāng)其中一個運(yùn)行有問題時不會影響另一個稍计。
如何構(gòu)造一個線程池對象
本文內(nèi)容我們只聊線程池ThreadPoolExecutor,查看它的源碼會發(fā)現(xiàn)它繼承了AbstractExecutorService抽象類裕循,而AbstractExecutorService實現(xiàn)了ExecutorService接口臣嚣,ExecutorService繼承了Executor接口,所以ThreadPoolExecutor間接實現(xiàn)了ExecutorService接口和Executor接口剥哑,它們的關(guān)系圖如下硅则。
一般我們使用的execute方法是在Executor接口中定義的,而submit方法是在ExecutorService接口中定義的株婴,所以當(dāng)我們創(chuàng)建一個Executor類型變量引用ThreadPoolExecutor對象實例時可以使用execute方法提交任務(wù)怎虫,當(dāng)我們創(chuàng)建一個ExecutorService類型變量時可以使用submit方法,當(dāng)然我們可以直接創(chuàng)建ThreadPoolExecutor類型變量使用execute方法或submit方法困介。
ThreadPoolExecutor定義了七大核心屬性大审,這些屬性是線程池實現(xiàn)的基石。corePoolSize(int):核心線程數(shù)量座哩。默認(rèn)情況下徒扶,在創(chuàng)建了線程池后,線程池中的線程數(shù)為0根穷,當(dāng)有任務(wù)來之后姜骡,就會創(chuàng)建一個線程去執(zhí)行任務(wù),當(dāng)線程池中的線程數(shù)目達(dá)到corePoolSize后屿良,就會把到達(dá)的任務(wù)放到任務(wù)隊列當(dāng)中溶浴。線程池將長期保證這些線程處于存活狀態(tài),即使線程已經(jīng)處于閑置狀態(tài)管引。除非配置了allowCoreThreadTimeOut=true士败,核心線程數(shù)的線程也將不再保證長期存活于線程池內(nèi),在空閑時間超過keepAliveTime后被銷毀。
workQueue:阻塞隊列谅将,存放等待執(zhí)行的任務(wù)漾狼,線程從workQueue中取任務(wù),若無任務(wù)將阻塞等待饥臂。當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize后逊躁,就會把新任務(wù)放到該隊列當(dāng)中。JDK提供了四個可直接使用的隊列實現(xiàn)隅熙,分別是:基于數(shù)組的有界隊列ArrayBlockingQueue稽煤、基于鏈表的無界隊列LinkedBlockingQueue、只有一個元素的同步隊列SynchronousQueue囚戚、優(yōu)先級隊列PriorityBlockingQueue酵熙。在實際使用時一定要設(shè)置隊列長度。
maximumPoolSize(int):線程池內(nèi)的最大線程數(shù)量驰坊,線程池內(nèi)維護(hù)的線程不得超過該數(shù)量匾二,大于核心線程數(shù)量小于最大線程數(shù)量的線程將在空閑時間超過keepAliveTime后被銷毀。當(dāng)阻塞隊列存滿后拳芙,將會創(chuàng)建新線程執(zhí)行任務(wù)察藐,線程的數(shù)量不會大于maximumPoolSize。
keepAliveTime(long):線程存活時間舟扎,若線程數(shù)超過了corePoolSize分飞,線程閑置時間超過了存活時間,該線程將被銷毀睹限。除非配置了allowCoreThreadTimeOut=true譬猫,核心線程數(shù)的線程也將不再保證長期存活于線程池內(nèi),在空閑時間超過keepAliveTime后被銷毀邦泄。
TimeUnit unit:線程存活時間的單位删窒,例如TimeUnit.SECONDS表示秒裂垦。
RejectedExecutionHandler:拒絕策略顺囊,當(dāng)任務(wù)隊列存滿并且線程池個數(shù)達(dá)到maximunPoolSize后采取的策略。ThreadPoolExecutor中提供了四種拒絕策略蕉拢,分別是:拋RejectedExecutionException異常的AbortPolicy(如果不指定的默認(rèn)策略)特碳、使用調(diào)用者所在線程來運(yùn)行任務(wù)CallerRunsPolicy、丟棄一個等待執(zhí)行的任務(wù)晕换,然后嘗試執(zhí)行當(dāng)前任務(wù)DiscardOldestPolicy午乓、不動聲色的丟棄并且不拋異常DiscardPolicy。項目中如果為了更多的用戶體驗闸准,可以自定義拒絕策略益愈。
threadFactory:創(chuàng)建線程的工廠,雖說JDK提供了線程工廠的默認(rèn)實現(xiàn)DefaultThreadFactory,但還是建議自定義實現(xiàn)最好蒸其,這樣可以自定義線程創(chuàng)建的過程敏释,例如線程分組、自定義線程名稱等摸袁。
一般我們使用類的構(gòu)造方法創(chuàng)建它的對象钥顽,ThreadPoolExecutor提供了四個構(gòu)造方法。
可以看到前三個方法最終都調(diào)用了最后一個靠汁、參數(shù)列表最長的那個方法蜂大,在這個方法中給七個屬性賦值。創(chuàng)建線程池對象蝶怔,強(qiáng)烈建議通過使用ThreadPoolExecutor的構(gòu)造方法創(chuàng)建奶浦,不要使用Executors,至于建議的理由上文中也有說過添谊,這里再引用阿里《Java開發(fā)手冊》中的一段描述财喳。
手?jǐn)]樣例
了解了線程池ThreadPoolExecutor的基本構(gòu)造,接下來手?jǐn)]一段代碼看看如何使用斩狱,樣例代碼中的參數(shù)僅為了配合原理解說使用耳高。
線程池工作原理
關(guān)于線程池的工作原理,我用下面的7幅圖來展示所踊。
1.通過execute方法提交任務(wù)時泌枪,當(dāng)線程池中的線程數(shù)小于corePoolSize時,新提交的任務(wù)將通過創(chuàng)建一個新線程來執(zhí)行秕岛,即使此時線程池中存在空閑線程碌燕。
2.通過execute方法提交任務(wù)時,當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize時继薛,新提交的任務(wù)將被放入workQueue中修壕,等待線程池中線程調(diào)度執(zhí)行。
3.通過execute方法提交任務(wù)時遏考,當(dāng)workQueue已存滿慈鸠,且maximumPoolSize大于corePoolSize時,新提交的任務(wù)將通過創(chuàng)建新線程執(zhí)行灌具。
4.當(dāng)線程池中的線程執(zhí)行完任務(wù)空閑時青团,會嘗試從workQueue中取頭結(jié)點任務(wù)執(zhí)行。
5.通過execute方法提交任務(wù)咖楣,當(dāng)線程池中線程數(shù)達(dá)到maxmumPoolSize督笆,并且workQueue也存滿時,新提交的任務(wù)由RejectedExecutionHandler執(zhí)行拒絕操作诱贿。
6.當(dāng)線程池中線程數(shù)超過corePoolSize娃肿,并且未配置allowCoreThreadTimeOut=true,空閑時間超過keepAliveTime的線程會被銷毀,保持線程池中線程數(shù)為corePoolSize料扰。
注意:上圖表達(dá)的是銷毀空閑線程锨阿,保持線程數(shù)為corePoolSize,不是銷毀corePoolSize中的線程记罚。
7.當(dāng)設(shè)置allowCoreThreadTimeOut=true時墅诡,任何空閑時間超過keepAliveTime的線程都會被銷毀。
線程池底層實現(xiàn)原理
查看ThreadPoolExecutor的源碼桐智,發(fā)現(xiàn)ThreadPoolExecutor的實現(xiàn)還是比較復(fù)雜的末早,下面簡單介紹幾個重要的全局常量和方法。
ctl用于表示線程池的狀態(tài)和線程數(shù)说庭,在ThreadPoolExecutor中使用32位二進(jìn)制數(shù)來表示線程池的狀態(tài)和線程池中線程數(shù)量然磷,其中前3位表示線程池狀態(tài),后29位表示線程池中線程數(shù)刊驴。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))初始化線程池狀態(tài)為RUNNING姿搜、線程池數(shù)量為0。
COUNT_BITS值等于Integer.SIZE - 3捆憎,在源碼中Integer.SIZE是32舅柜,所以COUNT_BITS=29。CAPACITY表示線程池允許的最大線程數(shù)躲惰,轉(zhuǎn)算后的結(jié)果如下致份。
RUNNING、SHUTDOWN础拨、STOP氮块、TIDYING和TERMINATED分別表示線程池的不同狀態(tài),轉(zhuǎn)算后的結(jié)果如下诡宗。
線程池處在不同的狀態(tài)時滔蝉,它的處理能力是不同的。
線程池不同狀態(tài)之間的轉(zhuǎn)換時機(jī)及轉(zhuǎn)換關(guān)系如下圖塔沃。
runStateOf獲取ctl高三位蝠引,也就是線程池的狀態(tài)。workerCountOf獲取ctl低29位芳悲,也就是線程池中線程數(shù)立肘。ctlOf計算ctlOf新值边坤,也就是線程池狀態(tài)和線程池個數(shù)名扛。
你可能會疑問“為什么要介紹上面這些?”茧痒,這是因為接下來的源碼分析會用到這些基礎(chǔ)的知識點肮韧。一般,我們使用ThreadPoolExecutor的execute方法提交任務(wù),所以從execute的源碼入手弄企。
為了更輕松的理解上圖中的源碼超燃,我又畫了一個流程圖。
到這里線程池的基本實現(xiàn)原理已經(jīng)很清晰了拘领,接下來我們重點分析一下線程池中線程是如何執(zhí)行任務(wù)意乓、如何復(fù)用線程和線程空閑時間超限如何判斷的。還是從execute方法入手约素,我們直接看它里面調(diào)用的addWorker方法届良,它實現(xiàn)了創(chuàng)建新線程執(zhí)行任務(wù)。
源碼中將線程和任務(wù)封裝到了Worker中圣猎,然后將Worker添加到HashSet集合中士葫,添加成功后通過線程對象的start方法啟動線程執(zhí)行任務(wù),既然這樣那我們就來看看上圖代碼中的w = new Worker(firstTask)到底是如何執(zhí)行的送悔。
Worker繼承了AbstractQueuedSynchronizer慢显,并且實現(xiàn)了Runnable接口,看到這里很清楚了任務(wù)最終由Worker中的run方法執(zhí)行欠啤,而run方法里調(diào)用了runWorker方法荚藻,所以重點還是runWorker方法。
在runWorker方法中洁段,使用循環(huán)鞋喇,通過getTask方法,不斷從阻塞隊列中獲取任務(wù)執(zhí)行眉撵,如果任務(wù)不為空則執(zhí)行任務(wù)侦香,這里實現(xiàn)了線程的復(fù)用,不斷的獲取任務(wù)執(zhí)行纽疟,不用重新創(chuàng)建線程罐韩;隊列中獲取的任務(wù)為null,則將Worker從HashSet集合中清除污朽,注意這個清除就是空閑線程的回收散吵。那getTask何時返回null?接著看getTask源碼蟆肆。
到這里矾睦,線程池中線程是如何執(zhí)行任務(wù)、如何復(fù)用線程炎功,以及線程空閑時間超限如何判斷都已經(jīng)清楚了枚冗。
最后,關(guān)于線程池的實現(xiàn)原理蛇损,我畫了一張思維導(dǎo)圖赁温。ps:如果平臺顯示的不是高清圖坛怪,可以在文末評論區(qū)或留言區(qū)@我,另外股囊,本文全圖文已收錄到GitHub:https://github.com/wind7rui/JavaHub袜匿,后續(xù)其它內(nèi)容也會更新到這里,歡迎follow稚疹、start居灯。
聊一聊實戰(zhàn)經(jīng)驗
使用構(gòu)造方法創(chuàng)建線程池
細(xì)心的朋友會發(fā)現(xiàn),全文竟沒有介紹Executors内狗,這個創(chuàng)建線程池的輔助工具類穆壕。是的,我強(qiáng)烈不推薦使用它其屏,因為Executors中的newFixedThreadPool和newSingleThreadExecutor方法創(chuàng)建的線程池中喇勋,阻塞隊列LinkedBlockingQueue的長度是Integer.MAX_VALUE,可能會堆積大量的任務(wù)偎行,從而導(dǎo)致 OOM川背;而newCachedThreadPool方法創(chuàng)建的線程池中最大線程數(shù)是Integer.MAX_VALUE,會創(chuàng)建大量的線程蛤袒,從而導(dǎo)致OOM熄云。如果創(chuàng)建線程池,通過ThreadPoolExecutor的構(gòu)造方法創(chuàng)建妙真,這樣使用這個線程池的人會更加明確線程池的各個參數(shù)的設(shè)置及運(yùn)行方式缴允,提前避免隱藏問題的發(fā)生。
使用自定義線程工廠
為什么要這么做呢珍德?是因為练般,當(dāng)項目規(guī)模逐漸擴(kuò)展,各系統(tǒng)中線程池也不斷增多锈候,當(dāng)發(fā)生線程執(zhí)行問題時薄料,通過自定義線程工廠創(chuàng)建的線程設(shè)置有意義的線程名稱可快速追蹤異常原因,高效泵琳、快速的定位問題摄职。
使用自定義拒絕策略
雖然,JDK給我們提供了一些默認(rèn)的拒絕策略获列,但我們可以根據(jù)項目需求的需要谷市,或者是用戶體驗的需要,定制拒絕策略击孩,完成特殊需求迫悠。
線程池劃分隔離
不同業(yè)務(wù)、執(zhí)行效率不同的分不同線程池溯壶,避免因某些異常導(dǎo)致整個線程池利用率下降或直接不可用及皂,進(jìn)而影響整個系統(tǒng)或其它系統(tǒng)的正常運(yùn)行。
小結(jié)
實際工作中且改,我們經(jīng)常使用線程池验烧,對這塊的要求不僅是常規(guī)的如何使用,原理我們也要清楚是怎么回事又跛。同時碍拆,線程池工作原理和底層實現(xiàn)原理也是面試必問的考題,所以慨蓝,這塊是一定要掌握的感混。
說實話,為了畫這些圖消耗了不少休息時間礼烈,如果你在看弧满,點個贊支持一下我的原創(chuàng)吧!
學(xué)之多此熬,而后知之少庭呜!朋友們點贊+轉(zhuǎn)發(fā)是我持續(xù)更新的最大動力,我們下期見犀忱!