開足碼力放妈,碼動(dòng)人生北救,本文首發(fā)公眾號【 Craig無忌 】,關(guān)注這個(gè)一言不合就開車的的代碼界老司機(jī)
本文 GitHub上已經(jīng)收錄 https://github.com/BeKingCoding/JavaKing 芜抒, 一線大廠面試核心知識點(diǎn)珍策、我的聯(lián)系方式和技術(shù)交流群,歡迎Star和完善
前言
昨天在群里有個(gè)同學(xué)問 Java 并發(fā)編程中的線程池內(nèi)容挽绩,本篇文章就給大家介紹下這個(gè)在面試中也經(jīng)常被問到的知識點(diǎn)膛壹。
看完后相信你會(huì)線程池的原理有更清晰的認(rèn)識。本文將會(huì)從以下幾個(gè)方面來講述相關(guān)知識,相信大家耐心看了之后肯定有收獲模聋,碼字不易肩民,別忘了「在看」,「轉(zhuǎn)發(fā)」哦链方。
- 為什么要使用線程池
- 線程池的工作原理
- 線程池的7大核心參數(shù)
- 如何正確地使用線程池
正文
**
01 為什么要使用線程池
**
引入一個(gè)技術(shù)之前持痰,首先應(yīng)該解答的問題是,這個(gè)技術(shù)解決什么問題祟蚀。
在 Java 語言中工窍,創(chuàng)建一個(gè)線程看上去非常簡單。實(shí)現(xiàn)Runnable接口前酿,然后像創(chuàng)建一個(gè)對象一樣患雏,直接 new Thread 就可以了。
但實(shí)際上線程的創(chuàng)建和銷毀遠(yuǎn)不是創(chuàng)建一個(gè)對象那么簡單罢维。線程的創(chuàng)建需要調(diào)用操作系統(tǒng)內(nèi)核的 API淹仑,然后操作系統(tǒng)為其分配一系列資源,所以整個(gè)成本很高肺孵,導(dǎo)致線程是一個(gè)重量級的對象匀借,應(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ìn)程運(yùn)行的特權(quán)級腋寨,大部分用戶直接面對的程序都是運(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)級最高铃剔。如果要執(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í)間夷磕。
為了避免資源過度消耗,所以最好的一種辦法就是對線程進(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 線程池的工作原理
線程池的工作原理可以簡化理解為以下幾個(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)目多少來增減工人码倦,那在編程世界里,如何定義忙和閑呢锭碳?
很簡單袁稽,當(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í)際工作中,自定義的拒絕策略往往和降級策略配合使用村象。
使用線程池笆环,需要注意異常處理的問題。任務(wù)在執(zhí)行的過程中出現(xiàn)運(yùn)行時(shí)異常厚者,會(huì)導(dǎo)致執(zhí)行任務(wù)的線程終止躁劣,最穩(wěn)妥和簡單的方案還是捕獲所有異常并按需處理。
需要注意的一點(diǎn)籍救,在《阿里巴巴Java開發(fā)手冊》也著重強(qiáng)調(diào)习绢,盡可能不要使用Executors 工具類來直接創(chuàng)建線程池,通過 ThreadPoolExecutor 的方式蝙昙,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)梧却。
Executors 返回的線程池對象的弊端如下:
(1)FixedThreadPool 和 SingleThreadPool 允許的請求隊(duì)列長度為 Integer.MAX_VALUE奇颠,可能會(huì)堆積大量的請求,從而導(dǎo)致 OOM放航。
(2)CachedThreadPool 和 ScheduledThreadPool 允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE烈拒,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致 OOM广鳍。
文末福利
最近各大互聯(lián)網(wǎng)公司的秋招都陸陸續(xù)續(xù)開始了荆几,還在找工作的小伙伴可以后臺回復(fù)關(guān)鍵字進(jìn)入對應(yīng)的秋招/內(nèi)推/面試群,我給大家整理了各大公司的內(nèi)推通道赊时、簡歷模板還有歷年的筆試題吨铸,大家要好好準(zhǔn)備哦。還可以幫助大家免費(fèi)修改簡歷祖秒、模擬面試哦~
關(guān)注公眾號「Craig無忌」
創(chuàng)作不易诞吱,各位的支持和認(rèn)可舟奠,就是我創(chuàng)作的最大動(dòng)力,我們下篇文章見房维!