開足碼力,碼動(dòng)人生,本文首發(fā)公眾號(hào)【 Craig無忌 】,關(guān)注這個(gè)一言不合就開車的的代碼界老司機(jī)
本文 GitHub上已經(jīng)收錄 https://github.com/BeKingCoding/JavaKing , 一線大廠面試核心知識(shí)點(diǎn)、我的聯(lián)系方式和技術(shù)交流群,歡迎Star和完善
前言
昨天在群里有個(gè)同學(xué)問 Java 并發(fā)編程中的線程池內(nèi)容,本篇文章就給大家介紹下這個(gè)在面試中也經(jīng)常被問到的知識(shí)點(diǎn)。
看完后相信你會(huì)線程池的原理有更清晰的認(rèn)識(shí)。本文將會(huì)從以下幾個(gè)方面來講述相關(guān)知識(shí),相信大家耐心看了之后肯定有收獲,碼字不易,別忘了「在看」,「轉(zhuǎn)發(fā)」哦。
- 為什么要使用線程池
- 線程池的工作原理
- 線程池的7大核心參數(shù)
- 如何正確地使用線程池
正文
**
01 為什么要使用線程池
**
引入一個(gè)技術(shù)之前,首先應(yīng)該解答的問題是,這個(gè)技術(shù)解決什么問題。
在 Java 語言中,創(chuàng)建一個(gè)線程看上去非常簡(jiǎn)單。實(shí)現(xiàn)Runnable接口,然后像創(chuàng)建一個(gè)對(duì)象一樣,直接 new Thread 就可以了。
但實(shí)際上線程的創(chuàng)建和銷毀遠(yuǎn)不是創(chuàng)建一個(gè)對(duì)象那么簡(jiǎn)單。線程的創(chuàng)建需要調(diào)用操作系統(tǒng)內(nèi)核的 API,然后操作系統(tǒng)為其分配一系列資源,所以整個(gè)成本很高,導(dǎo)致線程是一個(gè)重量級(jí)的對(duì)象,應(yīng)該避免頻繁創(chuàng)建和銷毀。
再來說說線程的上下文切換。
一個(gè) CPU 在一個(gè)時(shí)刻只能運(yùn)行一個(gè)線程,當(dāng)其運(yùn)行一個(gè)線程時(shí),由于時(shí)間片耗盡或出現(xiàn)阻塞等情況,CPU 會(huì)轉(zhuǎn)去執(zhí)行另外一個(gè)線程,這個(gè)叫做線程上下文切換。
并且當(dāng)前線程的任務(wù)可能并沒有執(zhí)行完畢,所以在進(jìn)行切換時(shí)需要保存線程的運(yùn)行狀態(tài),以便下次重新切換回來時(shí),能夠繼續(xù)切換之前的狀態(tài)運(yùn)行,這個(gè)過程就要涉及到用戶態(tài)和內(nèi)核態(tài)的切換。
什么是用戶態(tài)和內(nèi)核態(tài)?
當(dāng)在執(zhí)行用戶自己的代碼時(shí),則稱其處于用戶運(yùn)行態(tài)(用戶態(tài)),此時(shí)處理器特權(quán)級(jí)最低,是普通的用戶進(jìn)程運(yùn)行的特權(quán)級(jí),大部分用戶直接面對(duì)的程序都是運(yùn)行在用戶態(tài)。
當(dāng)因?yàn)橄到y(tǒng)調(diào)用陷入內(nèi)核代碼中執(zhí)行時(shí),處于內(nèi)核運(yùn)行態(tài)(內(nèi)核態(tài)),此時(shí)處理器處于特權(quán)級(jí)最高。如果要執(zhí)行文件操作、網(wǎng)絡(luò)數(shù)據(jù)發(fā)送等操作必須通過 write、send 等系統(tǒng)調(diào)用,這些系統(tǒng)調(diào)用會(huì)調(diào)用內(nèi)核的代碼。會(huì)從用戶態(tài)切換到內(nèi)核態(tài)的內(nèi)核地址空間去執(zhí)行內(nèi)核代碼來完成相應(yīng)的操作,在執(zhí)行完后又會(huì)切換回用戶態(tài)。
如果并發(fā)的線程數(shù)量很多,并且每個(gè)線程都是執(zhí)行一個(gè)時(shí)間很短的任務(wù)就結(jié)束了,這樣頻繁創(chuàng)建線程就會(huì)大大降低系統(tǒng)的效率,因?yàn)轭l繁創(chuàng)建線程和銷毀線程需要時(shí)間。
為了避免資源過度消耗,所以最好的一種辦法就是對(duì)線程進(jìn)行復(fù)用,它執(zhí)行完一個(gè)任務(wù),并不需要被銷毀,而是讓它繼續(xù)執(zhí)行其他任務(wù)。
線程池是一種線程的使用模式,帶來了一系列好處:
(1)避免了線程的重復(fù)創(chuàng)建與開銷帶來的資源消耗代價(jià)。
(2)提升了任務(wù)響應(yīng)速度,任務(wù)到達(dá)時(shí),直接選一個(gè)線程執(zhí)行而無需等待線程的創(chuàng)建。
(3)提高線程的可管理性,線程的統(tǒng)一分配和管理,也方便統(tǒng)一的監(jiān)控和調(diào)優(yōu)。
這就是線程池最核心的設(shè)計(jì)思路,復(fù)用線程,平攤線程的創(chuàng)建與銷毀的開銷代價(jià)。
02 線程池的工作原理
線程池的工作原理可以簡(jiǎn)化理解為以下幾個(gè)步驟:
(1)在線程池的內(nèi)部,會(huì)維護(hù)了一個(gè)阻塞隊(duì)列 workQueue 和一組工作線程,工作線程的個(gè)數(shù)可以在初始化線程池的時(shí)候來指定。
(2)用戶可以將需要完成的任務(wù)提交給線程池,任務(wù)會(huì)被加入到 workQueue中。
(3)線程池內(nèi)部維護(hù)的工作線程會(huì)按照次序,依次消費(fèi) workQueue 中的任務(wù)并進(jìn)行執(zhí)行,在執(zhí)行結(jié)束后并不會(huì)銷毀。
03 線程池的7大核心參數(shù)
我們可以通過 ThreadPoolExecutor 來創(chuàng)建線程池,創(chuàng)建的時(shí)候需要指定7大核心參數(shù),每一個(gè)參數(shù)都代表線程池的特定工作行為,非常重要。
corePoolSize(核心線程數(shù))
將把線程池類比為一個(gè)施工隊(duì),而線程就是施工隊(duì)的工人。有些時(shí)候比較閑,項(xiàng)目比較少,但是施工隊(duì)也不能把工人都遣散,需要留下一些核心骨干來以備不時(shí)之需,所以至少要留 corePoolSize 個(gè)人堅(jiān)守陣地。
corePoolSize 表示線程池保有的核心線程數(shù),核心線程會(huì)一直存活,即使這些線程處于空閑狀態(tài)沒有任務(wù)執(zhí)行,他們也不會(huì)被銷毀。
maximumPoolSize(最大線程數(shù))
當(dāng)項(xiàng)目比較多的時(shí)候,施工隊(duì)就需要增加工人,但是也不能無限制地加。最多就加到 maximumPoolSize 個(gè)人,當(dāng)閑下來的時(shí)候,施工隊(duì)就要遣散工人,但是至少保留corePoolSize 個(gè)人。
keepAliveTime&unit(存活時(shí)間&單位)
上面提到施工隊(duì)根據(jù)忙閑,項(xiàng)目多少來增減工人,那在編程世界里,如何定義忙和閑呢?
很簡(jiǎn)單,當(dāng)線程池內(nèi)部的線程數(shù)已經(jīng)大于 corePoolSize 的時(shí)候,一個(gè)線程如果在一段時(shí)間內(nèi),都沒有執(zhí)行任務(wù),說明很閑。
keepAliveTime 和 unit 就是用來定義這個(gè)“一段時(shí)間”的參數(shù)。也就是說,如果一個(gè)線程空閑了keepAliveTime & unit 這么久,那么這個(gè)空閑的線程就要被回收了。
workQueue(工作隊(duì)列)
新任務(wù)被提交后,會(huì)先進(jìn)入到此工作隊(duì)列中,任務(wù)調(diào)度時(shí)再從隊(duì)列中取出任務(wù)。
threadFactory(線程工廠)
創(chuàng)建一個(gè)新線程時(shí)使用的工廠,通過這個(gè)工廠可以自定義如何創(chuàng)建線程,例如可以給線程指定一個(gè)有意義的名字。
handler(拒絕策略)
如果線程池中所有的線程都在忙碌,并且工作隊(duì)列也滿了(前提是工作隊(duì)列是有界隊(duì)列),那么此時(shí)提交任務(wù),線程池就會(huì)拒絕接收。
至于拒絕的策略,可以通過 handler 這個(gè)參數(shù)來指定:
CallerRunsPolicy:提交任務(wù)的線程自己去執(zhí)行該任務(wù)。
AbortPolicy:默認(rèn)的拒絕策略,直接丟棄任務(wù),拋出RejectedExecutionException。
DiscardPolicy:直接丟棄任務(wù),沒有任何異常拋出。
DiscardOldestPolicy:丟棄最老的任務(wù),其實(shí)就是把最早進(jìn)入工作隊(duì)列的任務(wù)丟棄,然后把新任務(wù)加入到工作隊(duì)列。
04 如何正確地使用線程池
默認(rèn)的拒絕策略要慎重使用。如果線程池處理的任務(wù)非常重要,建議自定義自己的拒絕策略;并且在實(shí)際工作中,自定義的拒絕策略往往和降級(jí)策略配合使用。
使用線程池,需要注意異常處理的問題。任務(wù)在執(zhí)行的過程中出現(xiàn)運(yùn)行時(shí)異常,會(huì)導(dǎo)致執(zhí)行任務(wù)的線程終止,最穩(wěn)妥和簡(jiǎn)單的方案還是捕獲所有異常并按需處理。
需要注意的一點(diǎn),在《阿里巴巴Java開發(fā)手冊(cè)》也著重強(qiáng)調(diào),盡可能不要使用Executors 工具類來直接創(chuàng)建線程池,通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
Executors 返回的線程池對(duì)象的弊端如下:
(1)FixedThreadPool 和 SingleThreadPool 允許的請(qǐng)求隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致 OOM。
(2)CachedThreadPool 和 ScheduledThreadPool 允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致 OOM。
文末福利
最近各大互聯(lián)網(wǎng)公司的秋招都陸陸續(xù)續(xù)開始了,還在找工作的小伙伴可以后臺(tái)回復(fù)關(guān)鍵字進(jìn)入對(duì)應(yīng)的秋招/內(nèi)推/面試群,我給大家整理了各大公司的內(nèi)推通道、簡(jiǎn)歷模板還有歷年的筆試題,大家要好好準(zhǔn)備哦。還可以幫助大家免費(fèi)修改簡(jiǎn)歷、模擬面試哦~
關(guān)注公眾號(hào)「Craig無忌」
創(chuàng)作不易,各位的支持和認(rèn)可,就是我創(chuàng)作的最大動(dòng)力,我們下篇文章見!