| 好看請(qǐng)贊,養(yǎng)成習(xí)慣
你有一個(gè)思想愁铺,我有一個(gè)思想鹰霍,我們交換后,一個(gè)人就有兩個(gè)思想
If you can NOT explain it simply, you do NOT understand it well enough
現(xiàn)陸續(xù)將Demo代碼和技術(shù)文章整理在一起 Github實(shí)踐精選 茵乱,方便大家閱讀查看茂洒,本文同樣收錄在此,覺(jué)得不錯(cuò)似将,還請(qǐng)Star??
上一篇文章 面試問(wèn)我获黔,創(chuàng)建多少個(gè)線程合適蚀苛?我該怎么說(shuō) 從定性到定量的分析了如何創(chuàng)建正確個(gè)數(shù)的線程來(lái)最大化利用系統(tǒng)資源(其實(shí)就是幾道小學(xué)數(shù)學(xué)題)在验。通常來(lái)講,有了個(gè)這個(gè)知識(shí)點(diǎn)傍身堵未,按需手動(dòng)創(chuàng)建相應(yīng)個(gè)數(shù)的線程就好
但是現(xiàn)實(shí)中腋舌,你也許聽(tīng)過(guò)或者被要求:
盡量避免手動(dòng)創(chuàng)建線程,應(yīng)使用線程池統(tǒng)一管理線程
為什么會(huì)有這樣的要求渗蟹?背后的道理又是怎樣的呢块饺?順著這個(gè)經(jīng)驗(yàn)理論來(lái)推斷,那肯定是手動(dòng)創(chuàng)建線程有缺點(diǎn)
手動(dòng)創(chuàng)建線程有什么缺點(diǎn)雌芽?
- 不受控風(fēng)險(xiǎn)
- 頻繁創(chuàng)建開(kāi)銷大
不受控風(fēng)險(xiǎn)
這個(gè)缺點(diǎn)授艰,相信你也可以說(shuō)出一二
系統(tǒng)資源有限,每個(gè)人針對(duì)不同業(yè)務(wù)都可以手動(dòng)創(chuàng)建線程世落,并且創(chuàng)建標(biāo)準(zhǔn)不一樣(比如線程沒(méi)有名字)淮腾。當(dāng)系統(tǒng)運(yùn)行起來(lái),所有線程都在瘋狂搶占資源屉佳,無(wú)組織無(wú)紀(jì)律谷朝,混亂場(chǎng)面可想而知(出現(xiàn)問(wèn)題,自然也就不可能輕易的發(fā)現(xiàn)和解決)
如果有位神奇的小伙伴武花,為每個(gè)請(qǐng)求都創(chuàng)建一個(gè)線程圆凰,當(dāng)大量請(qǐng)求鋪面而來(lái)的時(shí)候,這好比一個(gè)正規(guī)木馬程序体箕,內(nèi)存被無(wú)情榨干耗盡(你無(wú)情专钉,你冷酷,你無(wú)理取鬧)
另外累铅,過(guò)多的線程自然也會(huì)引起上下文切換的開(kāi)銷
總的來(lái)說(shuō)驶沼,不受控風(fēng)險(xiǎn)很大
頻繁創(chuàng)建開(kāi)銷大
面試問(wèn):頻繁手動(dòng)創(chuàng)建線程有什么問(wèn)題?
答:開(kāi)銷大
這貌似是一個(gè)不假思索就可以回答出來(lái)的正確答案争群。那我要繼續(xù)問(wèn)了
面試官:創(chuàng)建一個(gè)線程干了什么就開(kāi)銷大了回怜?和我們創(chuàng)建一個(gè)普通 Java 對(duì)象有什么差別?
答: ... 嗯...啊
按照常規(guī)理解 new Thread() 創(chuàng)建一個(gè)線程和 new Object() 沒(méi)有什么差別。Java中萬(wàn)物接對(duì)象玉雾,因?yàn)?Thread 的老祖宗也是 Object
如果你真是這么理解的翔试,說(shuō)明你對(duì)線程的生命周期還不是很理解,請(qǐng)回看之前的 Java線程生命周期這樣理解挺簡(jiǎn)單的
在這篇文章中我們明確說(shuō)明复旬,new Thread() 在操作系統(tǒng)層面并沒(méi)有創(chuàng)建新的線程垦缅,這是編程語(yǔ)言特有的。真正轉(zhuǎn)換為操作系統(tǒng)層面創(chuàng)建一個(gè)線程驹碍,還要調(diào)用操作系統(tǒng)內(nèi)核的API壁涎,然后操作系統(tǒng)要為該線程分配一系列的資源
廢話不多說(shuō),我們將二者做個(gè)對(duì)比:
new Object() 過(guò)程
Object obj = new Object();
當(dāng)我需要【對(duì)象】時(shí)志秃,我就會(huì)給自己 new 一個(gè)(不知你是否和我一樣)怔球,這個(gè)過(guò)程你應(yīng)該很熟悉了:
- 分配一塊內(nèi)存 M
- 在內(nèi)存 M 上初始化該對(duì)象
- 將內(nèi)存 M 的地址賦值給引用變量 obj
就是這么簡(jiǎn)單
創(chuàng)建一個(gè)線程的過(guò)程
上面已經(jīng)提到了,創(chuàng)建一個(gè)線程還要調(diào)用操作系統(tǒng)內(nèi)核API浮还。為了更好的理解創(chuàng)建并啟動(dòng)一個(gè)線程的開(kāi)銷竟坛,我們需要看看 JVM 在背后幫我們做了哪些事情:
- 它為一個(gè)線程棧分配內(nèi)存,該棧為每個(gè)線程方法調(diào)用保存一個(gè)棧幀
- 每一棧幀由一個(gè)局部變量數(shù)組钧舌、返回值担汤、操作數(shù)堆棧和常量池組成
- 一些支持本機(jī)方法的 jvm 也會(huì)分配一個(gè)本機(jī)堆棧
- 每個(gè)線程獲得一個(gè)程序計(jì)數(shù)器,告訴它當(dāng)前處理器執(zhí)行的指令是什么
- 系統(tǒng)創(chuàng)建一個(gè)與Java線程對(duì)應(yīng)的本機(jī)線程
- 將與線程相關(guān)的描述符添加到JVM內(nèi)部數(shù)據(jù)結(jié)構(gòu)中
- 線程共享堆和方法區(qū)域
這段描述稍稍有點(diǎn)抽象洼冻,用數(shù)據(jù)來(lái)說(shuō)明創(chuàng)建一個(gè)線程(即便不干什么)需要多大空間呢崭歧?答案是大約 1M
左右
java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -version
上圖是我用 Java8 的測(cè)試結(jié)果,19個(gè)線程撞牢,預(yù)留和提交的大概都是19000+KB率碾,平均每個(gè)線程大概需要 1M 左右的大小(Java11的結(jié)果完全不同普泡,這個(gè)大家自行測(cè)試吧)
相信到這里你已經(jīng)明白了播掷,對(duì)于性能要求嚴(yán)苛的現(xiàn)在,頻繁手動(dòng)創(chuàng)建/銷毀線程的代價(jià)是非常巨大的撼班,解決方案自然也是你知道的線程池了
什么是線程池歧匈?
你常見(jiàn)的數(shù)據(jù)庫(kù)連接池,實(shí)例池砰嘁,還有XX池件炉,OO池,各種池矮湘,都是一種池化(pooling)思想斟冕,簡(jiǎn)而言之就是為了最大化收益,并最小化風(fēng)險(xiǎn)缅阳,將資源統(tǒng)一在一起管理的思想
Java 也提供了它自己實(shí)現(xiàn)的線程池模型—— ThreadPoolExecutor
磕蛇。套用上面池化的想象來(lái)說(shuō),Java線程池就是為了最大化高并發(fā)帶來(lái)的性能提升,并最小化手動(dòng)創(chuàng)建線程的風(fēng)險(xiǎn)秀撇,將多個(gè)線程統(tǒng)一在一起管理的思想
為了了解這個(gè)管理思想超棺,我們當(dāng)前只需要關(guān)注 ThreadPoolExecutor
構(gòu)造方法就可以了
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
這么復(fù)雜的構(gòu)造方法在JDK中還真是不多見(jiàn),為了個(gè)更形象化的讓大家理解這幾個(gè)核心參數(shù)呵燕,我們以多數(shù)人都經(jīng)歷過(guò)的春運(yùn)(北京——上海)來(lái)說(shuō)明
序號(hào) | 參數(shù)名稱 | 參數(shù)解釋 | 春運(yùn)形象說(shuō)明 |
---|---|---|---|
1 | corePoolSize | 表示常駐核心線程數(shù)棠绘,如果大于0,即使本地任務(wù)執(zhí)行完也不會(huì)被銷毀 | 日常固定的列車數(shù)輛(不管是不是春運(yùn)再扭,都要有固定這些車次運(yùn)行) |
2 | maximumPoolSize | 表示線程池能夠容納可同時(shí)執(zhí)行的最大線程數(shù) | 春運(yùn)客流量大氧苍,臨時(shí)加車,加車后泛范,總列車次數(shù)不能超過(guò)這個(gè)最大值让虐,否則就會(huì)出現(xiàn)調(diào)度不開(kāi)等問(wèn)題 (結(jié)合workqueue) |
3 | keepAliveTime | 表示線程池中線程空閑的時(shí)間,當(dāng)空閑時(shí)間達(dá)到該值時(shí)敦跌,線程會(huì)被銷毀澄干,只剩下 corePoolSize 個(gè)線程位置 |
春運(yùn)壓力過(guò)后逛揩,臨時(shí)的加車(如果空閑時(shí)間超過(guò)keepAliveTime )就會(huì)被撤掉柠傍,只保留日常固定的列車車次數(shù)量用于日常運(yùn)營(yíng) |
4 | unit |
keepAliveTime 的時(shí)間單位,最終都會(huì)轉(zhuǎn)換成【納秒】辩稽,因?yàn)镃PU的執(zhí)行速度杠杠滴 |
keepAliveTime 的單位惧笛,春運(yùn)以【天】為計(jì)算單位 |
5 | workQueue | 當(dāng)請(qǐng)求的線程數(shù)大于 corePoolSize 時(shí),線程進(jìn)入該阻塞隊(duì)列 |
春運(yùn)壓力異常大逞泄,(達(dá)到corePoolSize )也不能滿足要求患整,所有乘坐請(qǐng)求都會(huì)進(jìn)入該阻塞隊(duì)列中排隊(duì), 隊(duì)列滿,還有額外請(qǐng)求喷众,就需要加車了 |
6 | threadFactory | 顧名思義各谚,線程工廠,用來(lái)生產(chǎn)一組相同任務(wù)的線程到千,同時(shí)也可以通過(guò)它增加前綴名昌渤,虛擬機(jī)棧分析時(shí)更清晰 | 比如(北京——上海)就屬于該段列車所有前綴,表明列車運(yùn)輸職責(zé) |
7 | handler | 執(zhí)行拒絕策略憔四,當(dāng) workQueue 達(dá)到上限膀息,同時(shí)也達(dá)到 maximumPoolSize 就要通過(guò)這個(gè)來(lái)處理,比如拒絕了赵,丟棄等潜支,這是一種限流的保護(hù)措施 |
當(dāng)workQueue 排隊(duì)也達(dá)到隊(duì)列最大上線,maximumPoolSize 就要提示無(wú)票等拒絕策略了,因?yàn)槲覀儾荒芗榆嚵耸裂矗?dāng)前所有車次已經(jīng)滿負(fù)載 |
整體來(lái)看就是這樣:
試想冗酿,如果有請(qǐng)求就新建一趟列車,請(qǐng)求結(jié)束就“銷毀”這趟列車,頻繁往復(fù)這樣操作裁替,這樣的代價(jià)肯定是不能接受的鸠窗。
可以看到,使用線程池不但能完成手動(dòng)創(chuàng)建線程可以做到的工作胯究,同時(shí)也填補(bǔ)了手動(dòng)線程不能做到的空白稍计。歸納起來(lái)說(shuō),線程池的作用包括:
- 利用線程池管理并服用線程裕循,控制最大并發(fā)數(shù)(手動(dòng)創(chuàng)建線程很難得到保證)
- 實(shí)現(xiàn)任務(wù)線程隊(duì)列緩存策略和拒絕機(jī)制
- 實(shí)現(xiàn)某些與實(shí)踐相關(guān)的功能臣嚣,如定時(shí)執(zhí)行,周期執(zhí)行等(比如列車指定時(shí)間運(yùn)行)
- 隔離線程環(huán)境剥哑,比如硅则,交易服務(wù)和搜索服務(wù)在同一臺(tái)服務(wù)器上,分別開(kāi)啟兩個(gè)線程池株婴,交易線程的資源消耗明顯要大怎虫。因此,通過(guò)配置獨(dú)立的線程池困介,將較慢的交易服務(wù)與搜索服務(wù)個(gè)離開(kāi)大审,避免個(gè)服務(wù)線程互相影響
相信到這里,你已經(jīng)了解線程池的基本思想了座哩,在使用過(guò)程中還是有幾個(gè)注意事項(xiàng)要說(shuō)明一下的
線程池使用思想/注意事項(xiàng)
不能忽略的線程池拒絕策略
我們很難準(zhǔn)確的預(yù)測(cè)未來(lái)的最大并發(fā)量徒扶,所以定制合理的拒絕策略是必不可少的步驟。默認(rèn)情況根穷, ThreadPoolExecutor 提供了四種拒絕策略:
AbortPolicy:默認(rèn)的拒絕策略姜骡,會(huì) throw RejectedExecutionException 拒絕
CallerRunsPolicy:提交任務(wù)的線程自己去執(zhí)行該任務(wù)
DiscardOldestPolicy:丟棄最老的任務(wù),其實(shí)就是把最早進(jìn)入工作隊(duì)列的任務(wù)丟棄屿良,然后把新任務(wù)加入到工作隊(duì)列
DiscardPolicy:相當(dāng)大膽的策略圈澈,直接丟棄任務(wù),沒(méi)有任何異常拋出
不同的框架(Netty尘惧,Dubbo)都有不同的拒絕策略康栈,我們也可以通過(guò)實(shí)現(xiàn) RejectedExecutionHandler
自定義的拒絕策略
對(duì)于采用何種策略,具體要看執(zhí)行的任務(wù)重要程度褥伴。如果是一些不重要任務(wù)谅将,可以選擇直接丟棄;如果是重要任務(wù)重慢,可以采用降級(jí)(所謂降級(jí)就是在服務(wù)無(wú)法正常提供功能的情況下饥臂,采取的補(bǔ)救措施。具體采用何種降級(jí)手段似踱,這也是要看具體場(chǎng)景)處理隅熙,例如將任務(wù)信息插入數(shù)據(jù)庫(kù)或者消息隊(duì)列稽煤,啟用一個(gè)專門用作補(bǔ)償?shù)木€程池去進(jìn)行補(bǔ)償
沒(méi)有絕對(duì)的拒絕策略,只有適合那一個(gè)囚戚,但在設(shè)計(jì)過(guò)程中千萬(wàn)不要忽略掉拒絕策略就可以
禁止使用Executors創(chuàng)建線程池
相信很多人都看到過(guò)這個(gè)問(wèn)題(阿里巴巴Java開(kāi)發(fā)手冊(cè)說(shuō)明禁止使用 Executors 創(chuàng)建線程池)酵熙,我把出處(P247)截圖在此:
Executors 大大的簡(jiǎn)化了我們創(chuàng)建各種類型線程池的方式,為什么還不讓使用呢驰坊?
其實(shí)匾二,只要你打開(kāi)看看它的靜態(tài)方法參數(shù)就會(huì)明白了
傳入的workQueue 是一個(gè)邊界為 Integer.MAX_VALUE
隊(duì)列,我們也可以變相的稱之為無(wú)界隊(duì)列了拳芙,因?yàn)檫吔缣罅瞬烀辏@么大的等待隊(duì)列也是非常消耗內(nèi)存的
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
另外該 ThreadPoolExecutor方法使用的是默認(rèn)拒絕策略(直接拒絕),但并不是所有業(yè)務(wù)場(chǎng)景都適合使用這個(gè)策略舟扎,當(dāng)很重要的請(qǐng)求過(guò)來(lái)直接選擇拒絕顯然是不合適的
總的來(lái)說(shuō)分飞,使用 Executors 創(chuàng)建的線程池太過(guò)于理想化,并不能滿足很多現(xiàn)實(shí)中的業(yè)務(wù)場(chǎng)景睹限,所以要求我們通過(guò) ThreadPoolExecutor來(lái)創(chuàng)建譬猫,并傳入合適的參數(shù)
總結(jié)
當(dāng)我們需要頻繁的創(chuàng)建線程時(shí),我們要考慮到通過(guò)線程池統(tǒng)一管理線程資源羡疗,避免不可控風(fēng)險(xiǎn)以及額外的開(kāi)銷
了解了線程池的幾個(gè)核心參數(shù)概念后染服,我們也需要經(jīng)過(guò)調(diào)優(yōu)的過(guò)程來(lái)設(shè)置最佳線程參數(shù)值(這個(gè)過(guò)程時(shí)必不可少的)
線程池雖然彌補(bǔ)了手動(dòng)創(chuàng)建線程的缺陷和空白,同時(shí)顺囊,合理的降級(jí)策略能大大增加系統(tǒng)的穩(wěn)定性
阿里巴巴手冊(cè)都是前輩們無(wú)數(shù)填坑后總結(jié)的精華肌索,你也應(yīng)該遵守相應(yīng)的指示蕉拢,結(jié)合自己的實(shí)際業(yè)務(wù)場(chǎng)景特碳,設(shè)定合適的參數(shù)來(lái)創(chuàng)建線程池
靈魂追問(wèn)
- 我們說(shuō)了這么多線程池的好,那使用線程池有哪些缺點(diǎn)或限制呢晕换?
- 為什么不建議所有業(yè)務(wù)共用一個(gè)線程池午乓?有什么缺點(diǎn)?
- 給線程池設(shè)置指定前綴闸准,有哪些方式益愈?
參考
感謝前輩們總結(jié)的精華,自己所寫(xiě)的并發(fā)系列好多都參考了以下資料
- Java 并發(fā)編程實(shí)戰(zhàn)
- Java 并發(fā)編程之美
- 碼出高效
- Java 并發(fā)編程的藝術(shù)
- ifeve
- 美團(tuán)技術(shù)團(tuán)隊(duì)
個(gè)人博客:https://dayarch.top
加我微信好友, 進(jìn)群娛樂(lè)學(xué)習(xí)交流夷家,備注「進(jìn)群」
歡迎持續(xù)關(guān)注公眾號(hào):「日拱一兵」
- 前沿 Java 技術(shù)干貨分享
- 高效工具匯總 | 回復(fù)「工具」
- 面試問(wèn)題分析與解答
- 技術(shù)資料領(lǐng)取 | 回復(fù)「資料」
以讀偵探小說(shuō)思維輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識(shí)蒸其,本著將復(fù)雜問(wèn)題簡(jiǎn)單化,抽象問(wèn)題具體化和圖形化原則逐步分解技術(shù)問(wèn)題库快,技術(shù)持續(xù)更新摸袁,請(qǐng)持續(xù)關(guān)注......