上篇《這樣的API網(wǎng)關(guān)查詢接口優(yōu)化,我是被迫的》文章末尾,有朋友留言提到文中的場(chǎng)景是IO密集型操作,不是CPU密集操作,不需要使用線程池,我猜這位朋友可能想表達(dá)的是IO密集且阻塞時(shí)間久的不要使用線程池方案解決。IO密集型在控制好同步處理時(shí)間或阻塞等待的條件下是可以使用線程池的,不知道這么描述是否合理,有高見(jiàn)的大佬可以繼續(xù)留言討論。 關(guān)注過(guò)我更新頻率的朋友會(huì)發(fā)現(xiàn)有好幾天沒(méi)有上新內(nèi)容了,原因有二,一是最近真的太忙了,項(xiàng)目催的緊,程序員哪有不加班是吧;另一個(gè)是我正在梳理技能圖譜,后續(xù)的內(nèi)容更新會(huì)根據(jù)這個(gè)圖譜來(lái),還在進(jìn)行中,有興趣的朋友持續(xù)關(guān)注下我和我的github:wind7rui,持續(xù)更新哦!好了,開(kāi)始我們今天的話題~線程池。注意:下方多圖高能預(yù)警,建議先收藏后閱讀,防止走丟! 為什么要使用線程池平時(shí)討論多線程處理,大佬們必定會(huì)說(shuō)使用線程池,那為什么要使用線程池?其實(shí),這個(gè)問(wèn)題可以反過(guò)來(lái)思考一下,不使用線程池會(huì)怎么樣?當(dāng)需要多線程并發(fā)執(zhí)行任務(wù)時(shí),只能不斷的通過(guò)new Thread創(chuàng)建線程,每創(chuàng)建一個(gè)線程都需要在堆上分配內(nèi)存空間,同時(shí)需要分配虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器等線程私有的內(nèi)存空間,當(dāng)這個(gè)線程對(duì)象被可達(dá)性分析算法標(biāo)記為不可用時(shí)被GC回收,這樣頻繁的創(chuàng)建和回收需要大量的額外開(kāi)銷。再者說(shuō),JVM的內(nèi)存資源是有限的,如果系統(tǒng)中大量的創(chuàng)建線程對(duì)象,JVM很可能直接拋出OutOfMemoryError異常,還有大量的線程去競(jìng)爭(zhēng)CPU會(huì)產(chǎn)生其他的性能開(kāi)銷,更多的線程反而會(huì)降低性能,所以必須要限制線程數(shù)。 既然不使用線程池有那么多問(wèn)題,我們來(lái)看一下使用線程池有哪些好處:
如何構(gòu)造一個(gè)線程池對(duì)象本文內(nèi)容我們只聊線程池ThreadPoolExecutor,查看它的源碼會(huì)發(fā)現(xiàn)它繼承了AbstractExecutorService抽象類,而AbstractExecutorService實(shí)現(xiàn)了ExecutorService接口,ExecutorService繼承了Executor接口,所以ThreadPoolExecutor間接實(shí)現(xiàn)了ExecutorService接口和Executor接口,它們的關(guān)系圖如下。 一般我們使用的execute方法是在Executor接口中定義的,而submit方法是在ExecutorService接口中定義的,所以當(dāng)我們創(chuàng)建一個(gè)Executor類型變量引用ThreadPoolExecutor對(duì)象實(shí)例時(shí)可以使用execute方法提交任務(wù),當(dāng)我們創(chuàng)建一個(gè)ExecutorService類型變量時(shí)可以使用submit方法,當(dāng)然我們可以直接創(chuàng)建ThreadPoolExecutor類型變量使用execute方法或submit方法。 ThreadPoolExecutor定義了七大核心屬性,這些屬性是線程池實(shí)現(xiàn)的基石。 corePoolSize(int):核心線程數(shù)量。默認(rèn)情況下,在創(chuàng)建了線程池后,線程池中的線程數(shù)為0,當(dāng)有任務(wù)來(lái)之后,就會(huì)創(chuàng)建一個(gè)線程去執(zhí)行任務(wù),當(dāng)線程池中的線程數(shù)目達(dá)到corePoolSize后,就會(huì)把到達(dá)的任務(wù)放到任務(wù)隊(duì)列當(dāng)中。線程池將長(zhǎng)期保證這些線程處于存活狀態(tài),即使線程已經(jīng)處于閑置狀態(tài)。除非配置了allowCoreThreadTimeOut=true,核心線程數(shù)的線程也將不再保證長(zhǎng)期存活于線程池內(nèi),在空閑時(shí)間超過(guò)keepAliveTime后被銷毀。 workQueue:阻塞隊(duì)列,存放等待執(zhí)行的任務(wù),線程從workQueue中取任務(wù),若無(wú)任務(wù)將阻塞等待。當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize后,就會(huì)把新任務(wù)放到該隊(duì)列當(dāng)中。JDK提供了四個(gè)可直接使用的隊(duì)列實(shí)現(xiàn),分別是:基于數(shù)組的有界隊(duì)列ArrayBlockingQueue、基于鏈表的無(wú)界隊(duì)列LinkedBlockingQueue、只有一個(gè)元素的同步隊(duì)列SynchronousQueue、優(yōu)先級(jí)隊(duì)列PriorityBlockingQueue。在實(shí)際使用時(shí)一定要設(shè)置隊(duì)列長(zhǎng)度。 maximumPoolSize(int):線程池內(nèi)的最大線程數(shù)量,線程池內(nèi)維護(hù)的線程不得超過(guò)該數(shù)量,大于核心線程數(shù)量小于最大線程數(shù)量的線程將在空閑時(shí)間超過(guò)keepAliveTime后被銷毀。當(dāng)阻塞隊(duì)列存滿后,將會(huì)創(chuàng)建新線程執(zhí)行任務(wù),線程的數(shù)量不會(huì)大于maximumPoolSize。 keepAliveTime(long):線程存活時(shí)間,若線程數(shù)超過(guò)了corePoolSize,線程閑置時(shí)間超過(guò)了存活時(shí)間,該線程將被銷毀。除非配置了allowCoreThreadTimeOut=true,核心線程數(shù)的線程也將不再保證長(zhǎng)期存活于線程池內(nèi),在空閑時(shí)間超過(guò)keepAliveTime后被銷毀。 TimeUnit unit:線程存活時(shí)間的單位,例如TimeUnit.SECONDS表示秒。 RejectedExecutionHandler:拒絕策略,當(dāng)任務(wù)隊(duì)列存滿并且線程池個(gè)數(shù)達(dá)到maximunPoolSize后采取的策略。ThreadPoolExecutor中提供了四種拒絕策略,分別是:拋RejectedExecutionException異常的AbortPolicy(如果不指定的默認(rèn)策略)、使用調(diào)用者所在線程來(lái)運(yùn)行任務(wù)CallerRunsPolicy、丟棄一個(gè)等待執(zhí)行的任務(wù),然后嘗試執(zhí)行當(dāng)前任務(wù)DiscardOldestPolicy、不動(dòng)聲色的丟棄并且不拋異常DiscardPolicy。項(xiàng)目中如果為了更多的用戶體驗(yàn),可以自定義拒絕策略。 threadFactory:創(chuàng)建線程的工廠,雖說(shuō)JDK提供了線程工廠的默認(rèn)實(shí)現(xiàn)DefaultThreadFactory,但還是建議自定義實(shí)現(xiàn)最好,這樣可以自定義線程創(chuàng)建的過(guò)程,例如線程分組、自定義線程名稱等。 一般我們使用類的構(gòu)造方法創(chuàng)建它的對(duì)象,ThreadPoolExecutor提供了四個(gè)構(gòu)造方法。 可以看到前三個(gè)方法最終都調(diào)用了最后一個(gè)、參數(shù)列表最長(zhǎng)的那個(gè)方法,在這個(gè)方法中給七個(gè)屬性賦值。創(chuàng)建線程池對(duì)象,強(qiáng)烈建議通過(guò)使用ThreadPoolExecutor的構(gòu)造方法創(chuàng)建,不要使用Executors,至于建議的理由上文中也有說(shuō)過(guò),這里再引用阿里《Java開(kāi)發(fā)手冊(cè)》中的一段描述。 手?jǐn)]樣例了解了線程池ThreadPoolExecutor的基本構(gòu)造,接下來(lái)手?jǐn)]一段代碼看看如何使用,樣例代碼中的參數(shù)僅為了配合原理解說(shuō)使用。 線程池工作原理關(guān)于線程池的工作原理,我用下面的7幅圖來(lái)展示。 1.通過(guò)execute方法提交任務(wù)時(shí),當(dāng)線程池中的線程數(shù)小于corePoolSize時(shí),新提交的任務(wù)將通過(guò)創(chuàng)建一個(gè)新線程來(lái)執(zhí)行,即使此時(shí)線程池中存在空閑線程。 2.通過(guò)execute方法提交任務(wù)時(shí),當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize時(shí),新提交的任務(wù)將被放入workQueue中,等待線程池中線程調(diào)度執(zhí)行。 3.通過(guò)execute方法提交任務(wù)時(shí),當(dāng)workQueue已存滿,且maximumPoolSize大于corePoolSize時(shí),新提交的任務(wù)將通過(guò)創(chuàng)建新線程執(zhí)行。 4.當(dāng)線程池中的線程執(zhí)行完任務(wù)空閑時(shí),會(huì)嘗試從workQueue中取頭結(jié)點(diǎn)任務(wù)執(zhí)行。 5.通過(guò)execute方法提交任務(wù),當(dāng)線程池中線程數(shù)達(dá)到maxmumPoolSize,并且workQueue也存滿時(shí),新提交的任務(wù)由RejectedExecutionHandler執(zhí)行拒絕操作。 6.當(dāng)線程池中線程數(shù)超過(guò)corePoolSize,并且未配置allowCoreThreadTimeOut=true,空閑時(shí)間超過(guò)keepAliveTime的線程會(huì)被銷毀,保持線程池中線程數(shù)為corePoolSize。 注意:上圖表達(dá)的是銷毀空閑線程,保持線程數(shù)為corePoolSize,不是銷毀corePoolSize中的線程。 7.當(dāng)設(shè)置allowCoreThreadTimeOut=true時(shí),任何空閑時(shí)間超過(guò)keepAliveTime的線程都會(huì)被銷毀。 線程池底層實(shí)現(xiàn)原理查看ThreadPoolExecutor的源碼,發(fā)現(xiàn)ThreadPoolExecutor的實(shí)現(xiàn)還是比較復(fù)雜的,下面簡(jiǎn)單介紹幾個(gè)重要的全局常量和方法。 ctl用于表示線程池的狀態(tài)和線程數(shù),在ThreadPoolExecutor中使用32位二進(jìn)制數(shù)來(lái)表示線程池的狀態(tài)和線程池中線程數(shù)量,其中前3位表示線程池狀態(tài),后29位表示線程池中線程數(shù)。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))初始化線程池狀態(tài)為RUNNING、線程池?cái)?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)時(shí),它的處理能力是不同的。 線程池不同狀態(tài)之間的轉(zhuǎn)換時(shí)機(jī)及轉(zhuǎn)換關(guān)系如下圖。 runStateOf獲取ctl高三位,也就是線程池的狀態(tài)。workerCountOf獲取ctl低29位,也就是線程池中線程數(shù)。ctlOf計(jì)算ctlOf新值,也就是線程池狀態(tài)和線程池個(gè)數(shù)。 你可能會(huì)疑問(wèn)“為什么要介紹上面這些?”,這是因?yàn)榻酉聛?lái)的源碼分析會(huì)用到這些基礎(chǔ)的知識(shí)點(diǎn)。一般,我們使用ThreadPoolExecutor的execute方法提交任務(wù),所以從execute的源碼入手。 為了更輕松的理解上圖中的源碼,我又畫(huà)了一個(gè)流程圖。 到這里線程池的基本實(shí)現(xiàn)原理已經(jīng)很清晰了,接下來(lái)我們重點(diǎn)分析一下線程池中線程是如何執(zhí)行任務(wù)、如何復(fù)用線程和線程空閑時(shí)間超限如何判斷的。還是從execute方法入手,我們直接看它里面調(diào)用的addWorker方法,它實(shí)現(xiàn)了創(chuàng)建新線程執(zhí)行任務(wù)。 源碼中將線程和任務(wù)封裝到了Worker中,然后將Worker添加到HashSet集合中,添加成功后通過(guò)線程對(duì)象的start方法啟動(dòng)線程執(zhí)行任務(wù),既然這樣那我們就來(lái)看看上圖代碼中的w = new Worker(firstTask)到底是如何執(zhí)行的。 Worker繼承了AbstractQueuedSynchronizer,并且實(shí)現(xiàn)了Runnable接口,看到這里很清楚了任務(wù)最終由Worker中的run方法執(zhí)行,而run方法里調(diào)用了runWorker方法,所以重點(diǎn)還是runWorker方法。 在runWorker方法中,使用循環(huán),通過(guò)getTask方法,不斷從阻塞隊(duì)列中獲取任務(wù)執(zhí)行,如果任務(wù)不為空則執(zhí)行任務(wù),這里實(shí)現(xiàn)了線程的復(fù)用,不斷的獲取任務(wù)執(zhí)行,不用重新創(chuàng)建線程;隊(duì)列中獲取的任務(wù)為null,則將Worker從HashSet集合中清除,注意這個(gè)清除就是空閑線程的回收。那getTask何時(shí)返回null?接著看getTask源碼。 到這里,線程池中線程是如何執(zhí)行任務(wù)、如何復(fù)用線程,以及線程空閑時(shí)間超限如何判斷都已經(jīng)清楚了。 最后,關(guān)于線程池的實(shí)現(xiàn)原理,我畫(huà)了一張思維導(dǎo)圖。ps:如果平臺(tái)顯示的不是高清圖,可以在文末評(píng)論區(qū)或留言區(qū)@我,另外,本文全圖文已收錄到GitHub:wind7rui,后續(xù)其它內(nèi)容也會(huì)更新到這里,歡迎follow、start。 聊一聊實(shí)戰(zhàn)經(jīng)驗(yàn)使用構(gòu)造方法創(chuàng)建線程池 細(xì)心的朋友會(huì)發(fā)現(xiàn),全文竟沒(méi)有介紹Executors,這個(gè)創(chuàng)建線程池的輔助工具類。是的,我強(qiáng)烈不推薦使用它,因?yàn)镋xecutors中的newFixedThreadPool和newSingleThreadExecutor方法創(chuàng)建的線程池中,阻塞隊(duì)列LinkedBlockingQueue的長(zhǎng)度是Integer.MAX_VALUE,可能會(huì)堆積大量的任務(wù),從而導(dǎo)致 OOM;而newCachedThreadPool方法創(chuàng)建的線程池中最大線程數(shù)是Integer.MAX_VALUE,會(huì)創(chuàng)建大量的線程,從而導(dǎo)致OOM。如果創(chuàng)建線程池,通過(guò)ThreadPoolExecutor的構(gòu)造方法創(chuàng)建,這樣使用這個(gè)線程池的人會(huì)更加明確線程池的各個(gè)參數(shù)的設(shè)置及運(yùn)行方式,提前避免隱藏問(wèn)題的發(fā)生。 使用自定義線程工廠 為什么要這么做呢?是因?yàn)?,?dāng)項(xiàng)目規(guī)模逐漸擴(kuò)展,各系統(tǒng)中線程池也不斷增多,當(dāng)發(fā)生線程執(zhí)行問(wèn)題時(shí),通過(guò)自定義線程工廠創(chuàng)建的線程設(shè)置有意義的線程名稱可快速追蹤異常原因,高效、快速的定位問(wèn)題。 使用自定義拒絕策略 雖然,JDK給我們提供了一些默認(rèn)的拒絕策略,但我們可以根據(jù)項(xiàng)目需求的需要,或者是用戶體驗(yàn)的需要,定制拒絕策略,完成特殊需求。 線程池劃分隔離 不同業(yè)務(wù)、執(zhí)行效率不同的分不同線程池,避免因某些異常導(dǎo)致整個(gè)線程池利用率下降或直接不可用,進(jìn)而影響整個(gè)系統(tǒng)或其它系統(tǒng)的正常運(yùn)行。 小結(jié)實(shí)際工作中,我們經(jīng)常使用線程池,對(duì)這塊的要求不僅是常規(guī)的如何使用,原理我們也要清楚是怎么回事。同時(shí),線程池工作原理和底層實(shí)現(xiàn)原理也是面試必問(wèn)的考題,所以,這塊是一定要掌握的。 說(shuō)實(shí)話,為了畫(huà)這些圖消耗了不少休息時(shí)間,如果你在看,點(diǎn)個(gè)贊支持一下我的原創(chuàng)吧! 學(xué)之多,而后知之少!朋友們點(diǎn)贊+轉(zhuǎn)發(fā)是我持續(xù)更新的最大動(dòng)力,我們下期見(jiàn)! |
|