一、線程池簡(jiǎn)介
在實(shí)際開發(fā)中憾朴,如果每個(gè)請(qǐng)求到達(dá)就創(chuàng)建一個(gè)新線程狸捕,開銷是相當(dāng)大的。服務(wù)器在創(chuàng)建和銷毀線程上花費(fèi)的時(shí)間和消耗的系統(tǒng)資源都相當(dāng)大众雷,甚至可能要比在處理實(shí)際的用請(qǐng)求的時(shí)間和資源要多的多灸拍。除了創(chuàng)建和銷毀線程的開銷之外,活動(dòng)的線程也需要消耗系統(tǒng)資源砾省。如果在一個(gè)jvm里創(chuàng)建太多的線程鸡岗,可能會(huì)使系統(tǒng)由于過(guò)度消耗內(nèi)存或“切換過(guò)度”而導(dǎo)致系統(tǒng)資源不足。這就引入了線程池概念编兄。
??線程池的核心思想就是:連接復(fù)用轩性,通過(guò)建立一個(gè)數(shù)據(jù)庫(kù)連接池以及一套連接使用、分配狠鸳、管理策略揣苏,使得該線程池中的連接可以得到高效悯嗓、安全的復(fù)用,避免了數(shù)據(jù)庫(kù)連接頻繁建立卸察、關(guān)閉的開銷绅作。
??在線程池中,它自己維護(hù)一些數(shù)據(jù)連接蛾派,需要使用的時(shí)候直接使用其中一個(gè)連接俄认,用完之后不是關(guān)閉而是將它歸還,等待其他操作洪乍。
二眯杏、線程池的實(shí)現(xiàn)
2.1 線程池實(shí)現(xiàn)簡(jiǎn)介
??在Java1.5中提供了一個(gè)非常高效實(shí)用的多線程包:java.util.concurrent,提供了大量高級(jí)工具,可以幫助開發(fā)者編寫高效易維護(hù)壳澳、結(jié)構(gòu)清晰的Java多線程程序岂贩。這個(gè)是JDK自帶實(shí)現(xiàn)線程池的包。
??Java里面線程池的頂級(jí)接口是Executor巷波,但是嚴(yán)格意義上講Executor并不是一個(gè)線程池萎津,而只是一個(gè)執(zhí)行線程的工具。真正的線程池接口是ExecutorService抹镊。比較重要的幾個(gè)類:
類名 | 接口 |
---|---|
ExecutorService | 真正的線程池接口锉屈。 |
ScheduledExecutorService | 能和Timer/TimerTask類似,解決那些需要任務(wù)重復(fù)執(zhí)行的問(wèn)題垮耳。 |
ThreadPoolExecutor | ExecutorService的默認(rèn)實(shí)現(xiàn)(底層實(shí)現(xiàn))颈渊。 |
ScheduledThreadPoolExecutor | 繼承ThreadPoolExecutor的ScheduledExecutorService接口實(shí)現(xiàn),周期性任務(wù)調(diào)度的類實(shí)現(xiàn)终佛。 |
一個(gè)線程池包括以下四個(gè)基本組成部分:
1俊嗽、線程池管理器(ThreadPool):用于創(chuàng)建并管理線程池,包括創(chuàng)建線程池铃彰,銷毀線程池绍豁,添加新任務(wù);
2牙捉、工作線程(PoolWorker):線程池中線程竹揍,在沒(méi)有任務(wù)時(shí)處于等待狀態(tài),可以循環(huán)的執(zhí)行任務(wù)鹃共;
3鬼佣、任務(wù)接口(Task):每個(gè)任務(wù)必須實(shí)現(xiàn)的接口,以供工作線程調(diào)度任務(wù)的執(zhí)行霜浴,它主要規(guī)定了任務(wù)的入口晶衷,任務(wù)執(zhí)行完后的收尾工作,任務(wù)的執(zhí)行狀態(tài)等;
4晌纫、任務(wù)隊(duì)列(taskQueue):用于存放沒(méi)有處理的任務(wù)税迷。提供一種緩沖機(jī)制。
要配置一個(gè)線程池是比較復(fù)雜的锹漱,尤其是對(duì)于線程池的原理不是很清楚的情況下箭养,很有可能配置的線程池不是較優(yōu)的,因此在Executors類里面提供了一些靜態(tài)工廠哥牍,生成一些常用的線程池:
1.創(chuàng)建一個(gè)單線程的線程池毕泌。這個(gè)線程池只有一個(gè)線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)嗅辣。如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束撼泛,那么會(huì)有一個(gè)新的線程來(lái)替代它。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行澡谭。
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
2.創(chuàng)建固定大小的線程池愿题。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大小蛙奖。線程池的大小一旦達(dá)到最大值就會(huì)保持不變潘酗,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束,那么線程池會(huì)補(bǔ)充一個(gè)新線程雁仲。
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
3.調(diào)度型線程池,調(diào)度型線程池會(huì)根據(jù)Scheduled(任務(wù)列表)進(jìn)行延遲執(zhí)行仔夺,或者是進(jìn)行周期性的執(zhí)行.適用于一些周期性的工作。
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
4.創(chuàng)建一個(gè)可緩存的線程池伯顶。如果線程池的大小超過(guò)了處理任務(wù)所需要的線程囚灼,那么就會(huì)回收部分空閑(60秒不執(zhí)行任務(wù))的線程骆膝,當(dāng)任務(wù)數(shù)增加時(shí)祭衩,此線程池又可以智能的添加新線程來(lái)處理任務(wù)。此線程池不會(huì)對(duì)線程池大小做限制阅签,線程池大小完全依賴于操作系統(tǒng)(或者說(shuō)JVM)能夠創(chuàng)建的最大線程大小掐暮。
ExecutorService executorService4 = Executors.newCacheThreadPool();
2.2 簡(jiǎn)單的代碼示例
Executor.java
public class Executor {
public static void main(String[] args) {
//定義了線程池中最大存在的線程數(shù)目
ExecutorService executorService=Executors.newFixedThreadPool(10);
//添加一個(gè)新的任務(wù)
for (int i = 0; i < 10; i++){
executorService.execute(new Begincode());
}
executor.shutdown();
}
}
Begincode.java
//無(wú)返回值的任務(wù)就是一個(gè)實(shí)現(xiàn)了runnable接口的類.使用run方法
public class Begincode implements Runnable {
public void run() {
System.out.println(“Begincode--runable”);
}
}
//有返回值的任務(wù)是一個(gè)實(shí)現(xiàn)了callable接口的類.使用call方法
public class Begincode implements callable{
public void call() {
System.out.println(“Begincode--callable”);
}
}
代碼說(shuō)明:
1.ExecutorService接口對(duì)象來(lái)執(zhí)行任務(wù),該對(duì)象有兩個(gè)方法可以執(zhí)行任務(wù)execute和submit政钟。
- execute這種方式提交沒(méi)有返回值路克,也就不能判斷是否執(zhí)行成功。
- submit這種方式它會(huì)返回一個(gè)Future對(duì)象养交。通過(guò)future的get方法來(lái)獲取返回值精算,get方法會(huì)阻塞住直到任務(wù)完成。
2.當(dāng)我們不需要使用線程池的時(shí)候碎连,我們需要對(duì)其進(jìn)行關(guān)閉灰羽。有兩種方法可以關(guān)閉掉線程池。
- shutdown():并不是直接關(guān)閉線程池,而是不再接受新的任務(wù)廉嚼。如果線程池內(nèi)有任務(wù)玫镐,那么把這些任務(wù)執(zhí)行完畢后,關(guān)閉線程池怠噪。
- shutdownNow():這個(gè)方法表示不再接受新的任務(wù)恐似,并把任務(wù)隊(duì)列中的任務(wù)直接移出掉,如果有正在執(zhí)行的傍念,嘗試進(jìn)行停止矫夷。
2.3 阻塞隊(duì)列
??JDK使用了實(shí)現(xiàn)接口BlockingQueue的阻塞隊(duì)列來(lái)存儲(chǔ)待處理工作job,并把隊(duì)列作為構(gòu)造函數(shù)參數(shù)憋槐,從而實(shí)現(xiàn)業(yè)務(wù)可以靈活的擴(kuò)展定制線程池的隊(duì)列口四。業(yè)務(wù)也可使用JDK自身同步阻塞隊(duì)列SynchronousQueue、有界隊(duì)列ArrayBlockingQueue秦陋、無(wú)界隊(duì)列LinkedBlockingQueue蔓彩。
- SynchronousQueue是無(wú)界的,也就是說(shuō)他存數(shù)任務(wù)的能力是沒(méi)有限制的驳概,但是由于該Queue本身的特性赤嚼,在某次添加元素后必須等待其他線程取走后才能繼續(xù)添加。如果運(yùn)行的線程等于或多于 corePoolSize顺又,則 Executor始終首選將請(qǐng)求加入隊(duì)列更卒,而不添加新的線程;如果無(wú)法將請(qǐng)求加入隊(duì)列稚照,則創(chuàng)建新的線程蹂空,除非創(chuàng)建此線程超出maximumPoolSize,在這種情況下果录,任務(wù)將被拒絕上枕。
- LinkedBlockingQueue創(chuàng)建的線程就不會(huì)超過(guò) corePoolSize。如果運(yùn)行的線程少于 corePoolSize弱恒,則 Executor 始終首選添加新的線程辨萍,而不進(jìn)行排隊(duì)。如果運(yùn)行的線程等于或多于 corePoolSize返弹,則 Executor 始終首選將請(qǐng)求加入隊(duì)列锈玉,而不添加新的線程。換句說(shuō)义起,永遠(yuǎn)也不會(huì)觸發(fā)產(chǎn)生新的線程拉背!corePoolSize大小的線程數(shù)會(huì)一直運(yùn)行,忙完當(dāng)前的默终,就從隊(duì)列中拿任務(wù)開始運(yùn)行椅棺。所以要防止任務(wù)瘋長(zhǎng)抡诞,比如任務(wù)運(yùn)行的實(shí)行比較長(zhǎng),而添加任務(wù)的速度遠(yuǎn)遠(yuǎn)超過(guò)處理任務(wù)的時(shí)間土陪,而且還不斷增加昼汗,不一會(huì)兒就爆了。
- ArrayBlockingQueue這個(gè)是最為復(fù)雜的使用鬼雀,所以JDK不推薦使用也有些道理顷窒。與上面的相比,最大的特點(diǎn)便是可以防止資源耗盡的情況發(fā)生源哩。
三鞋吉、線程池帶來(lái)的好處
- 降低資源消耗。通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗励烦;
- 提高響應(yīng)速度谓着。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行坛掠;
- 提高線程的可管理性赊锚。線程是稀缺資源,如果無(wú)限制的創(chuàng)建屉栓,不僅會(huì)消耗系統(tǒng)資源舷蒲,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配友多,調(diào)優(yōu)和監(jiān)控牲平。
- 線程池可以應(yīng)對(duì)突然大爆發(fā)量的訪問(wèn),通過(guò)有限個(gè)固定線程為大量的操作服務(wù)域滥,減少創(chuàng)建和銷毀線程所需的時(shí)間纵柿。
四、使用線程池的風(fēng)險(xiǎn)
雖然線程池是構(gòu)建多線程應(yīng)用程序的強(qiáng)大機(jī)制启绰,但使用它并不是沒(méi)有風(fēng)險(xiǎn)的昂儒。用線程池構(gòu)建的應(yīng)用程序容易遭受任何其它多線程應(yīng)用程序容易遭受的所有并發(fā)風(fēng)險(xiǎn),諸如同步錯(cuò)誤和死鎖酬土,它還容易遭受特定于線程池的少數(shù)其它風(fēng)險(xiǎn)荆忍,諸如與池有關(guān)的死鎖、資源不足和線程泄漏撤缴。
死鎖
??雖然任何多線程程序中都有死鎖的風(fēng)險(xiǎn),但線程池卻引入了另一種死鎖可能叽唱。這可能會(huì)導(dǎo)致死鎖屈呕,在那種死鎖中,所有線程都被一些任務(wù)所占用棺亭,而這些線程又在排隊(duì)虎眨,同步等待其他任務(wù)結(jié)果,而這些任務(wù)又無(wú)法執(zhí)行,因?yàn)樗械木€程都很忙嗽桩。
資源不足
??線程消耗包括內(nèi)存和其它系統(tǒng)資源在內(nèi)的大量資源岳守。雖然線程之間切換的調(diào)度開銷很小,但如果有很多線程碌冶,環(huán)境切換也可能嚴(yán)重地影響程序的性能湿痢。
??線程池大小決定了在指定時(shí)間內(nèi)能夠處理的并發(fā)請(qǐng)求數(shù)。如果線程池太大扑庞,那么被那些線程消耗的資源可能嚴(yán)重地影響系統(tǒng)性能譬重。在線程之間進(jìn)行切換將會(huì)浪費(fèi)時(shí)間譬猫,而且使用超出比您實(shí)際需要的線程可能會(huì)引起資源匱乏問(wèn)題伐蒋,因?yàn)槌刂芯€程正在消耗一些資源壮不,而這些資源可能會(huì)被其它任務(wù)更有效地利用乘碑。
??除了線程自身所使用的資源以外痢毒,服務(wù)請(qǐng)求時(shí)所做的工作可能需要其它資源渣聚,例如 JDBC 連接阳惹、套接字或文件的榛。這些也都是有限資源租悄,有太多的并發(fā)請(qǐng)求也可能引起失效邑遏。如果一個(gè) web 應(yīng)用接收到的請(qǐng)求數(shù)高于線程池大小,多出來(lái)的請(qǐng)求將進(jìn)入隊(duì)列等待恰矩,或被拒絕记盒。
并發(fā)錯(cuò)誤
??線程池和其它排隊(duì)機(jī)制依靠使用 wait() 和 notify() 方法,這兩個(gè)方法都難于使用外傅。如果編碼不正確纪吮,那么可能丟失通知,導(dǎo)致線程保持空閑狀態(tài)萎胰,盡管隊(duì)列中有工作要處理碾盟。使用這些方法時(shí),必須格外小心技竟;即便是專家也可能在它們上面出錯(cuò)冰肴。而最好使用現(xiàn)有的、已經(jīng)知道能工作的實(shí)現(xiàn)榔组,例如使用無(wú)須編寫您自己的池中討論的util.concurrent 包熙尉。
線程泄露
??各種類型的線程池中一個(gè)嚴(yán)重的風(fēng)險(xiǎn)是線程泄漏,當(dāng)從池中除去一個(gè)線程以執(zhí)行一項(xiàng)任務(wù)搓扯,而在任務(wù)完成后該線程卻沒(méi)有返回池時(shí)检痰,會(huì)發(fā)生這種情況。發(fā)生線程泄漏的一種情形出現(xiàn)在任務(wù)拋出一個(gè) RuntimeException 或一個(gè) Error 時(shí)锨推。如果池類沒(méi)有捕捉到它們铅歼,那么線程只會(huì)退出而線程池的大小將會(huì)永久減少一個(gè)公壤。當(dāng)這種情況發(fā)生的次數(shù)足夠多時(shí),線程池最終就為空椎椰,而且系統(tǒng)將停止厦幅,因?yàn)闆](méi)有可用的線程來(lái)處理任務(wù)。
??有些任務(wù)可能會(huì)永遠(yuǎn)等待某些資源或來(lái)自用戶的輸入慨飘,而這些資源又不能保證變得可用确憨,用戶可能也已經(jīng)回家了,諸如此類的任務(wù)會(huì)永久停止套媚,而這些停止的任務(wù)也會(huì)引起和線程泄漏同樣的問(wèn)題缚态。如果某個(gè)線程被這樣一個(gè)任務(wù)永久地消耗著,那么它實(shí)際上就被從池除去了堤瘤。對(duì)于這樣的任務(wù)玫芦,應(yīng)該要么只給予它們自己的線程,要么只讓它們等待有限的時(shí)間本辐。
五桥帆、使用線程池的準(zhǔn)則
①不要把那些同步等待其它任務(wù)結(jié)果的任務(wù)線程加入隊(duì)列排隊(duì),因?yàn)榭赡芤l(fā)死鎖慎皱。
②在為時(shí)間可能很長(zhǎng)的操作使用合用的線程時(shí)要小心老虫。如果程序必須等待諸如 I/O 完成這樣的某個(gè)資源,那么請(qǐng)指定最長(zhǎng)的等待時(shí)間茫多,以及隨后是失效還是將任務(wù)重新排隊(duì)以便稍后執(zhí)行祈匙。
③根據(jù)任務(wù)相應(yīng)地調(diào)整線程池大小
??要有效地調(diào)整線程池大小,您需要理解正在排隊(duì)的任務(wù)以及它們正在做什么天揖。它們是 CPU 限制的(CPU-bound)嗎夺欲?它們是 I/O 限制的(I/O-bound)嗎?您的答案將影響您如何調(diào)整應(yīng)用程序今膊。
??調(diào)整線程池的大小基本上就是避免兩類錯(cuò)誤:線程太少或線程太多些阅。幸運(yùn)的是,對(duì)于大多數(shù)應(yīng)用程序來(lái)說(shuō)斑唬,太多和太少之間的余地相當(dāng)寬市埋。
??一個(gè)系統(tǒng)最快的部分是CPU,所以決定一個(gè)系統(tǒng)吞吐量上限的是CPU恕刘。增強(qiáng)CPU處理能力缤谎,可以提高系統(tǒng)吞吐量上限。但根據(jù)短板效應(yīng)雪营,真實(shí)的系統(tǒng)吞吐量并不能單純根據(jù)CPU來(lái)計(jì)算弓千。那要提高系統(tǒng)吞吐量,就需要從“系統(tǒng)短板”(比如網(wǎng)絡(luò)延遲献起、IO)著手:
- 盡量提高短板操作的并行化比率洋访,比如多線程下載技術(shù)
- 增強(qiáng)短板能力,比如用NIO替代IO
CPU密集型應(yīng)用
??CPU密集則是大量CPU時(shí)間都用于進(jìn)行計(jì)算谴餐。需要進(jìn)行矩陣運(yùn)算視頻解碼這些操作的通常屬于CPU密集姻政。觀察CPU占用的話多數(shù)時(shí)間都是出于I/O wait狀態(tài)(圖中綠色或黃色)。
I/O密集型應(yīng)用
??一個(gè)I/O密集的應(yīng)用通常行為是反復(fù)去讀寫磁盤文件(圖中藍(lán)色)岂嗓。
通常汁展,線程等待時(shí)間所占比例越高,需要越多線程厌殉。線程CPU時(shí)間所占比例越高食绿,需要越少線程。
??一般說(shuō)來(lái)公罕,大家認(rèn)為線程池的大小經(jīng)驗(yàn)值應(yīng)該這樣設(shè)置:(其中N為CPU的個(gè)數(shù))
- 如果是CPU密集型應(yīng)用器紧,則線程池大小設(shè)置為N+1
- 如果是IO密集型應(yīng)用,則線程池大小設(shè)置為2N+1
如果一臺(tái)服務(wù)器上只部署這一個(gè)應(yīng)用并且只有這一個(gè)線程池楼眷,那么這種估算或許合理铲汪,具體還需自行測(cè)試驗(yàn)證。但是罐柳,IO優(yōu)化中掌腰,確定線程池的大小相對(duì)比較復(fù)雜,涉及到下游系統(tǒng)的響應(yīng)時(shí)間张吉,因?yàn)橐粋€(gè)線程常常因?yàn)榈却渌到y(tǒng)的響應(yīng)而被阻塞齿梁。所以我們必須增加線程的數(shù)量以更好地利用CPU,所以這樣的估算公式可能更適合:
??最佳線程數(shù)目 = (線程等待時(shí)間與線程CPU時(shí)間之比 + 1) CPU數(shù)目*
很顯然肮蛹,線程等待時(shí)間所占比例越高勺择,需要越多線程。線程CPU時(shí)間所占比例越高蔗崎,需要越少線程酵幕。
六、使用線程池就一定比使用單線程高效缓苛?
答案是否定的芳撒,比如Redis就是單線程的,但它卻非常高效未桥,基本操作都能達(dá)到十萬(wàn)量級(jí)/s笔刹。從線程這個(gè)角度來(lái)看,部分原因在于:多線程帶來(lái)線程上下文切換開銷冬耿,單線程就沒(méi)有這種開銷舌菜。
??當(dāng)然“Redis很快”更本質(zhì)的原因在于:Redis基本都是內(nèi)存操作,這種情況下單線程可以很高效地利用CPU亦镶。而多線程適用場(chǎng)景一般是:存在相當(dāng)比例的IO和網(wǎng)絡(luò)操作日月。