我會(huì)手動(dòng)創(chuàng)建線(xiàn)程,為什么讓我使用線(xiàn)程池?
好看請(qǐng)贊,養(yǎng)成習(xí)慣
- 你有一個(gè)思想,我有一個(gè)思想,我們交換后,一個(gè)人就有兩個(gè)思想
- If you can NOT explain it simply, you do NOT understand it well enough
上一篇文章 面試問(wèn)我,創(chuàng)建多少個(gè)線(xiàn)程合適?我該怎么說(shuō) 從定性到定量的分析了如何創(chuàng)建正確個(gè)數(shù)的線(xiàn)程來(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)程就好
但是現(xiàn)實(shí)中,你也許聽(tīng)過(guò)或者被要求:
盡量避免手動(dòng)創(chuàng)建線(xiàn)程,應(yīng)使用線(xiàn)程池統(tǒng)一管理線(xiàn)程
為什么會(huì)有這樣的要求?背后的道理又是怎樣的呢?順著這個(gè)經(jīng)驗(yàn)理論來(lái)推斷,那肯定是手動(dòng)創(chuàng)建線(xiàn)程有缺點(diǎn)
手動(dòng)創(chuàng)建線(xiàn)程有什么缺點(diǎn)?
- 不受控風(fēng)險(xiǎn)
- 頻繁創(chuàng)建開(kāi)銷(xiāo)大
不受控風(fēng)險(xiǎn)
這個(gè)缺點(diǎn),相信你也可以說(shuō)出一二
系統(tǒng)資源有限,每個(gè)人針對(duì)不同業(yè)務(wù)都可以手動(dòng)創(chuàng)建線(xiàn)程,并且創(chuàng)建標(biāo)準(zhǔn)不一樣(比如線(xiàn)程沒(méi)有名字)。當(dāng)系統(tǒng)運(yùn)行起來(lái),所有線(xiàn)程都在瘋狂搶占資源,無(wú)組織無(wú)紀(jì)律,混亂場(chǎng)面可想而知(出現(xiàn)問(wèn)題,自然也就不可能輕易的發(fā)現(xiàn)和解決)
如果有位神奇的小伙伴,為每個(gè)請(qǐng)求都創(chuàng)建一個(gè)線(xiàn)程,當(dāng)大量請(qǐng)求鋪面而來(lái)的時(shí)候,這好比一個(gè)正規(guī)木馬程序,內(nèi)存被無(wú)情榨干耗盡(你無(wú)情,你冷酷,你無(wú)理取鬧)
另外,過(guò)多的線(xiàn)程自然也會(huì)引起上下文切換的開(kāi)銷(xiāo)
總的來(lái)說(shuō),不受控風(fēng)險(xiǎn)很大
頻繁創(chuàng)建開(kāi)銷(xiāo)大
面試問(wèn): 頻繁手動(dòng)創(chuàng)建線(xiàn)程有什么問(wèn)題?
答: 開(kāi)銷(xiāo)大
這貌似是一個(gè)不假思索就可以回答出來(lái)的正確答案。那我要繼續(xù)問(wèn)了
面試官: 創(chuàng)建一個(gè)線(xiàn)程干了什么就開(kāi)銷(xiāo)大了?和我們創(chuàng)建一個(gè)普通 Java 對(duì)象有什么差別?
答: ... 嗯...啊
按照常規(guī)理解 new Thread() 創(chuàng)建一個(gè)線(xiàn)程和 new Object() 沒(méi)有什么差別。Java中萬(wàn)物接對(duì)象,因?yàn)?Thread 的老祖宗也是 Object
如果你真是這么理解的,說(shuō)明你對(duì)線(xiàn)程的生命周期還不是很理解,請(qǐng)回看之前的 Java線(xiàn)程生命周期這樣理解挺簡(jiǎn)單的
在這篇文章中我們明確說(shuō)明,new Thread() 在操作系統(tǒng)層面并沒(méi)有創(chuàng)建新的線(xiàn)程,這是編程語(yǔ)言特有的。真正轉(zhuǎn)換為操作系統(tǒng)層面創(chuàng)建一個(gè)線(xiàn)程,還要調(diào)用操作系統(tǒng)內(nèi)核的API,然后操作系統(tǒng)要為該線(xiàn)程分配一系列的資源
廢話(huà)不多說(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è)線(xiàn)程的過(guò)程
上面已經(jīng)提到了,創(chuàng)建一個(gè)線(xiàn)程還要調(diào)用操作系統(tǒng)內(nèi)核API。為了更好的理解創(chuàng)建并啟動(dòng)一個(gè)線(xiàn)程的開(kāi)銷(xiāo),我們需要看看 JVM 在背后幫我們做了哪些事情:
- 它為一個(gè)線(xiàn)程棧分配內(nèi)存,該棧為每個(gè)線(xiàn)程方法調(diào)用保存一個(gè)棧幀
- 每一棧幀由一個(gè)局部變量數(shù)組、返回值、操作數(shù)堆棧和常量池組成
- 一些支持本機(jī)方法的 jvm 也會(huì)分配一個(gè)本機(jī)堆棧
- 每個(gè)線(xiàn)程獲得一個(gè)程序計(jì)數(shù)器,告訴它當(dāng)前處理器執(zhí)行的指令是什么
- 系統(tǒng)創(chuàng)建一個(gè)與Java線(xiàn)程對(duì)應(yīng)的本機(jī)線(xiàn)程
- 將與線(xiàn)程相關(guān)的描述符添加到JVM內(nèi)部數(shù)據(jù)結(jié)構(gòu)中
- 線(xiàn)程共享堆和方法區(qū)域
這段描述稍稍有點(diǎn)抽象,用數(shù)據(jù)來(lái)說(shuō)明創(chuàng)建一個(gè)線(xiàn)程(即便不干什么)需要多大空間呢?答案是大約 1M 左右
- java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -version
上圖是我用 Java8 的測(cè)試結(jié)果,19個(gè)線(xiàn)程,預(yù)留和提交的大概都是19000+KB,平均每個(gè)線(xiàn)程大概需要 1M 左右的大小(Java11的結(jié)果完全不同,這個(gè)大家自行測(cè)試吧)
相信到這里你已經(jīng)明白了,對(duì)于性能要求嚴(yán)苛的現(xiàn)在,頻繁手動(dòng)創(chuàng)建/銷(xiāo)毀線(xiàn)程的代價(jià)是非常巨大的,解決方案自然也是你知道的線(xiàn)程池了
什么是線(xiàn)程池?
你常見(jiàn)的數(shù)據(jù)庫(kù)連接池,實(shí)例池,還有XX池,OO池,各種池,都是一種池化(pooling)思想,簡(jiǎn)而言之就是為了最大化收益,并最小化風(fēng)險(xiǎn),將資源統(tǒng)一在一起管理的思想
Java 也提供了它自己實(shí)現(xiàn)的線(xiàn)程池模型—— ThreadPoolExecutor。套用上面池化的想象來(lái)說(shuō),Java線(xiàn)程池就是為了最大化高并發(fā)帶來(lái)的性能提升,并最小化手動(dòng)創(chuàng)建線(xiàn)程的風(fēng)險(xiǎn),將多個(gè)線(xiàn)程統(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ù)名稱(chēng) | 參數(shù)解釋 | 春運(yùn)形象說(shuō)明 |
---|---|---|---|
1 | corePoolSize | 表示常駐核心線(xiàn)程數(shù),如果大于0,即使本地任務(wù)執(zhí)行完也不會(huì)被銷(xiāo)毀 | 日常固定的列車(chē)數(shù)輛(不管是不是春運(yùn),都要有固定這些車(chē)次運(yùn)行) |
2 | maximumPoolSize | 表示線(xiàn)程池能夠容納可同時(shí)執(zhí)行的最大線(xiàn)程數(shù) | 春運(yùn)客流量大,臨時(shí)加車(chē),加車(chē)后,總列車(chē)次數(shù)不能超過(guò)這個(gè)最大值,否則就會(huì)出現(xiàn)調(diào)度不開(kāi)等問(wèn)題 (結(jié)合workqueue) |
3 | keepAliveTime | 表示線(xiàn)程池中線(xiàn)程空閑的時(shí)間,當(dāng)空閑時(shí)間達(dá)到該值時(shí),線(xiàn)程會(huì)被銷(xiāo)毀,只剩下 corePoolSize 個(gè)線(xiàn)程位置 |
春運(yùn)壓力過(guò)后,臨時(shí)的加車(chē)(如果空閑時(shí)間超過(guò)keepAliveTime )就會(huì)被撤掉,只保留日常固定的列車(chē)車(chē)次數(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)求的線(xiàn)程數(shù)大于 corePoolSize 時(shí),線(xiàn)程進(jìn)入該阻塞隊(duì)列 |
春運(yùn)壓力異常大,(達(dá)到corePoolSize )也不能滿(mǎn)足要求,所有乘坐請(qǐng)求都會(huì)進(jìn)入該阻塞隊(duì)列中排隊(duì), 隊(duì)列滿(mǎn),還有額外請(qǐng)求,就需要加車(chē)了 |
6 | threadFactory | 顧名思義,線(xiàn)程工廠(chǎng),用來(lái)生產(chǎn)一組相同任務(wù)的線(xiàn)程,同時(shí)也可以通過(guò)它增加前綴名,虛擬機(jī)棧分析時(shí)更清晰 | 比如(北京——上海)就屬于該段列車(chē)所有前綴,表明列車(chē)運(yùn)輸職責(zé) |
7 | handler | 執(zhí)行拒絕策略,當(dāng) workQueue 達(dá)到上限,同時(shí)也達(dá)到 maximumPoolSize 就要通過(guò)這個(gè)來(lái)處理,比如拒絕,丟棄等,這是一種限流的保護(hù)措施 |
當(dāng)workQueue 排隊(duì)也達(dá)到隊(duì)列最大上線(xiàn),maximumPoolSize 就要提示無(wú)票等拒絕策略了,因?yàn)槲覀儾荒芗榆?chē)了,當(dāng)前所有車(chē)次已經(jīng)滿(mǎn)負(fù)載 |
整體來(lái)看就是這樣:
試想,如果有請(qǐng)求就新建一趟列車(chē),請(qǐng)求結(jié)束就“銷(xiāo)毀”這趟列車(chē),頻繁往復(fù)這樣操作,這樣的代價(jià)肯定是不能接受的。
可以看到,使用線(xiàn)程池不但能完成手動(dòng)創(chuàng)建線(xiàn)程可以做到的工作,同時(shí)也填補(bǔ)了手動(dòng)線(xiàn)程不能做到的空白。歸納起來(lái)說(shuō),線(xiàn)程池的作用包括:
- 利用線(xiàn)程池管理并服用線(xiàn)程,控制最大并發(fā)數(shù)(手動(dòng)創(chuàng)建線(xiàn)程很難得到保證)
- 實(shí)現(xiàn)任務(wù)線(xiàn)程隊(duì)列緩存策略和拒絕機(jī)制
- 實(shí)現(xiàn)某些與實(shí)踐相關(guān)的功能,如定時(shí)執(zhí)行,周期執(zhí)行等(比如列車(chē)指定時(shí)間運(yùn)行)
- 隔離線(xiàn)程環(huán)境,比如,交易服務(wù)和搜索服務(wù)在同一臺(tái)服務(wù)器上,分別開(kāi)啟兩個(gè)線(xiàn)程池,交易線(xiàn)程的資源消耗明顯要大。因此,通過(guò)配置獨(dú)立的線(xiàn)程池,將較慢的交易服務(wù)與搜索服務(wù)個(gè)離開(kāi),避免個(gè)服務(wù)線(xiàn)程互相影響
相信到這里,你已經(jīng)了解線(xiàn)程池的基本思想了,在使用過(guò)程中還是有幾個(gè)注意事項(xiàng)要說(shuō)明一下的
線(xiàn)程池使用思想/注意事項(xiàng)
不能忽略的線(xiàn)程池拒絕策略
我們很難準(zhǔn)確的預(yù)測(cè)未來(lái)的最大并發(fā)量,所以定制合理的拒絕策略是必不可少的步驟。默認(rèn)情況, ThreadPoolExecutor 提供了四種拒絕策略:
- AbortPolicy:默認(rèn)的拒絕策略,會(huì) throw RejectedExecutionException 拒絕
- CallerRunsPolicy:提交任務(wù)的線(xiàn)程自己去執(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è)專(zhuān)門(mén)用作補(bǔ)償?shù)木€(xiàn)程池去進(jìn)行補(bǔ)償
沒(méi)有絕對(duì)的拒絕策略,只有適合那一個(gè),但在設(shè)計(jì)過(guò)程中千萬(wàn)不要忽略掉拒絕策略就可以
禁止使用Executors創(chuàng)建線(xiàn)程池
相信很多人都看到過(guò)這個(gè)問(wèn)題(阿里巴巴Java開(kāi)發(fā)手冊(cè)說(shuō)明禁止使用 Executors 創(chuàng)建線(xiàn)程池),我把出處(P247)截圖在此:
Executors 大大的簡(jiǎn)化了我們創(chuàng)建各種類(lèi)型線(xiàn)程池的方式,為什么還不讓使用呢?
其實(shí),只要你打開(kāi)看看它的靜態(tài)方法參數(shù)就會(huì)明白了
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
傳入的workQueue 是一個(gè)邊界為 Integer.MAX_VALUE 隊(duì)列,我們也可以變相的稱(chēng)之為無(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)直接選擇拒絕顯然是不合適的
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue) {
- this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
- Executors.defaultThreadFactory(), defaultHandler);
- }
總的來(lái)說(shuō),使用 Executors 創(chuàng)建的線(xiàn)程池太過(guò)于理想化,并不能滿(mǎn)足很多現(xiàn)實(shí)中的業(yè)務(wù)場(chǎng)景,所以要求我們通過(guò) ThreadPoolExecutor來(lái)創(chuàng)建,并傳入合適的參數(shù)
總結(jié)
當(dāng)我們需要頻繁的創(chuàng)建線(xiàn)程時(shí),我們要考慮到通過(guò)線(xiàn)程池統(tǒng)一管理線(xiàn)程資源,避免不可控風(fēng)險(xiǎn)以及額外的開(kāi)銷(xiāo)
了解了線(xiàn)程池的幾個(gè)核心參數(shù)概念后,我們也需要經(jīng)過(guò)調(diào)優(yōu)的過(guò)程來(lái)設(shè)置最佳線(xiàn)程參數(shù)值(這個(gè)過(guò)程時(shí)必不可少的)
線(xiàn)程池雖然彌補(bǔ)了手動(dòng)創(chuàng)建線(xiàn)程的缺陷和空白,同時(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)建線(xiàn)程池