一、線程池簡介
在實際開發(fā)中,如果每個請求到達就創(chuàng)建一個新線程,開銷是相當大的。服務器在創(chuàng)建和銷毀線程上花費的時間和消耗的系統(tǒng)資源都相當大,甚至可能要比在處理實際的用請求的時間和資源要多的多。除了創(chuàng)建和銷毀線程的開銷之外,活動的線程也需要消耗系統(tǒng)資源。如果在一個jvm里創(chuàng)建太多的線程,可能會使系統(tǒng)由于過度消耗內(nèi)存或“切換過度”而導致系統(tǒng)資源不足。這就引入了線程池概念。
??線程池的核心思想就是:連接復用,通過建立一個數(shù)據(jù)庫連接池以及一套連接使用、分配、管理策略,使得該線程池中的連接可以得到高效、安全的復用,避免了數(shù)據(jù)庫連接頻繁建立、關閉的開銷。
??在線程池中,它自己維護一些數(shù)據(jù)連接,需要使用的時候直接使用其中一個連接,用完之后不是關閉而是將它歸還,等待其他操作。
二、線程池的實現(xiàn)
2.1 線程池實現(xiàn)簡介
??在Java1.5中提供了一個非常高效實用的多線程包:java.util.concurrent,提供了大量高級工具,可以幫助開發(fā)者編寫高效易維護、結構清晰的Java多線程程序。這個是JDK自帶實現(xiàn)線程池的包。
??Java里面線程池的頂級接口是Executor,但是嚴格意義上講Executor并不是一個線程池,而只是一個執(zhí)行線程的工具。真正的線程池接口是ExecutorService。比較重要的幾個類:
| 類名 | 接口 |
|---|---|
| ExecutorService | 真正的線程池接口。 |
| ScheduledExecutorService | 能和Timer/TimerTask類似,解決那些需要任務重復執(zhí)行的問題。 |
| ThreadPoolExecutor | ExecutorService的默認實現(xiàn)(底層實現(xiàn))。 |
| ScheduledThreadPoolExecutor | 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現(xiàn),周期性任務調(diào)度的類實現(xiàn)。 |
一個線程池包括以下四個基本組成部分:
1、線程池管理器(ThreadPool):用于創(chuàng)建并管理線程池,包括創(chuàng)建線程池,銷毀線程池,添加新任務;
2、工作線程(PoolWorker):線程池中線程,在沒有任務時處于等待狀態(tài),可以循環(huán)的執(zhí)行任務;
3、任務接口(Task):每個任務必須實現(xiàn)的接口,以供工作線程調(diào)度任務的執(zhí)行,它主要規(guī)定了任務的入口,任務執(zhí)行完后的收尾工作,任務的執(zhí)行狀態(tài)等;
4、任務隊列(taskQueue):用于存放沒有處理的任務。提供一種緩沖機制。
要配置一個線程池是比較復雜的,尤其是對于線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優(yōu)的,因此在Executors類里面提供了一些靜態(tài)工廠,生成一些常用的線程池:
1.創(chuàng)建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當于單線程串行執(zhí)行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執(zhí)行順序按照任務的提交順序執(zhí)行。
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
2.創(chuàng)建固定大小的線程池。每次提交一個任務就創(chuàng)建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執(zhí)行異常而結束,那么線程池會補充一個新線程。
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
3.調(diào)度型線程池,調(diào)度型線程池會根據(jù)Scheduled(任務列表)進行延遲執(zhí)行,或者是進行周期性的執(zhí)行.適用于一些周期性的工作。
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
4.創(chuàng)建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑(60秒不執(zhí)行任務)的線程,當任務數(shù)增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小。
ExecutorService executorService4 = Executors.newCacheThreadPool();
2.2 簡單的代碼示例
Executor.java
public class Executor {
public static void main(String[] args) {
//定義了線程池中最大存在的線程數(shù)目
ExecutorService executorService=Executors.newFixedThreadPool(10);
//添加一個新的任務
for (int i = 0; i < 10; i++){
executorService.execute(new Begincode());
}
executor.shutdown();
}
}
Begincode.java
//無返回值的任務就是一個實現(xiàn)了runnable接口的類.使用run方法
public class Begincode implements Runnable {
public void run() {
System.out.println(“Begincode--runable”);
}
}
//有返回值的任務是一個實現(xiàn)了callable接口的類.使用call方法
public class Begincode implements callable{
public void call() {
System.out.println(“Begincode--callable”);
}
}
代碼說明:
1.ExecutorService接口對象來執(zhí)行任務,該對象有兩個方法可以執(zhí)行任務execute和submit。
- execute這種方式提交沒有返回值,也就不能判斷是否執(zhí)行成功。
- submit這種方式它會返回一個Future對象。通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成。
2.當我們不需要使用線程池的時候,我們需要對其進行關閉。有兩種方法可以關閉掉線程池。
- shutdown():并不是直接關閉線程池,而是不再接受新的任務。如果線程池內(nèi)有任務,那么把這些任務執(zhí)行完畢后,關閉線程池。
- shutdownNow():這個方法表示不再接受新的任務,并把任務隊列中的任務直接移出掉,如果有正在執(zhí)行的,嘗試進行停止。
2.3 阻塞隊列
??JDK使用了實現(xiàn)接口BlockingQueue的阻塞隊列來存儲待處理工作job,并把隊列作為構造函數(shù)參數(shù),從而實現(xiàn)業(yè)務可以靈活的擴展定制線程池的隊列。業(yè)務也可使用JDK自身同步阻塞隊列SynchronousQueue、有界隊列ArrayBlockingQueue、無界隊列LinkedBlockingQueue。
- SynchronousQueue是無界的,也就是說他存數(shù)任務的能力是沒有限制的,但是由于該Queue本身的特性,在某次添加元素后必須等待其他線程取走后才能繼續(xù)添加。如果運行的線程等于或多于 corePoolSize,則 Executor始終首選將請求加入隊列,而不添加新的線程;如果無法將請求加入隊列,則創(chuàng)建新的線程,除非創(chuàng)建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕。
- LinkedBlockingQueue創(chuàng)建的線程就不會超過 corePoolSize。如果運行的線程少于 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。如果運行的線程等于或多于 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程。換句說,永遠也不會觸發(fā)產(chǎn)生新的線程!corePoolSize大小的線程數(shù)會一直運行,忙完當前的,就從隊列中拿任務開始運行。所以要防止任務瘋長,比如任務運行的實行比較長,而添加任務的速度遠遠超過處理任務的時間,而且還不斷增加,不一會兒就爆了。
- ArrayBlockingQueue這個是最為復雜的使用,所以JDK不推薦使用也有些道理。與上面的相比,最大的特點便是可以防止資源耗盡的情況發(fā)生。
三、線程池帶來的好處
- 降低資源消耗。通過重復利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗;
- 提高響應速度。當任務到達時,任務可以不需要等到線程創(chuàng)建就能立即執(zhí)行;
- 提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
- 線程池可以應對突然大爆發(fā)量的訪問,通過有限個固定線程為大量的操作服務,減少創(chuàng)建和銷毀線程所需的時間。
四、使用線程池的風險
雖然線程池是構建多線程應用程序的強大機制,但使用它并不是沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有并發(fā)風險,諸如同步錯誤和死鎖,它還容易遭受特定于線程池的少數(shù)其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。
死鎖
??雖然任何多線程程序中都有死鎖的風險,但線程池卻引入了另一種死鎖可能。這可能會導致死鎖,在那種死鎖中,所有線程都被一些任務所占用,而這些線程又在排隊,同步等待其他任務結果,而這些任務又無法執(zhí)行,因為所有的線程都很忙。
資源不足
??線程消耗包括內(nèi)存和其它系統(tǒng)資源在內(nèi)的大量資源。雖然線程之間切換的調(diào)度開銷很小,但如果有很多線程,環(huán)境切換也可能嚴重地影響程序的性能。
??線程池大小決定了在指定時間內(nèi)能夠處理的并發(fā)請求數(shù)。如果線程池太大,那么被那些線程消耗的資源可能嚴重地影響系統(tǒng)性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏問題,因為池中線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。
??除了線程自身所使用的資源以外,服務請求時所做的工作可能需要其它資源,例如 JDBC 連接、套接字或文件。這些也都是有限資源,有太多的并發(fā)請求也可能引起失效。如果一個 web 應用接收到的請求數(shù)高于線程池大小,多出來的請求將進入隊列等待,或被拒絕。
并發(fā)錯誤
??線程池和其它排隊機制依靠使用 wait() 和 notify() 方法,這兩個方法都難于使用。如果編碼不正確,那么可能丟失通知,導致線程保持空閑狀態(tài),盡管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現(xiàn)有的、已經(jīng)知道能工作的實現(xiàn),例如使用無須編寫您自己的池中討論的util.concurrent 包。
線程泄露
??各種類型的線程池中一個嚴重的風險是線程泄漏,當從池中除去一個線程以執(zhí)行一項任務,而在任務完成后該線程卻沒有返回池時,會發(fā)生這種情況。發(fā)生線程泄漏的一種情形出現(xiàn)在任務拋出一個 RuntimeException 或一個 Error 時。如果池類沒有捕捉到它們,那么線程只會退出而線程池的大小將會永久減少一個。當這種情況發(fā)生的次數(shù)足夠多時,線程池最終就為空,而且系統(tǒng)將停止,因為沒有可用的線程來處理任務。
??有些任務可能會永遠等待某些資源或來自用戶的輸入,而這些資源又不能保證變得可用,用戶可能也已經(jīng)回家了,諸如此類的任務會永久停止,而這些停止的任務也會引起和線程泄漏同樣的問題。如果某個線程被這樣一個任務永久地消耗著,那么它實際上就被從池除去了。對于這樣的任務,應該要么只給予它們自己的線程,要么只讓它們等待有限的時間。
五、使用線程池的準則
①不要把那些同步等待其它任務結果的任務線程加入隊列排隊,因為可能引發(fā)死鎖。
②在為時間可能很長的操作使用合用的線程時要小心。如果程序必須等待諸如 I/O 完成這樣的某個資源,那么請指定最長的等待時間,以及隨后是失效還是將任務重新排隊以便稍后執(zhí)行。
③根據(jù)任務相應地調(diào)整線程池大小
??要有效地調(diào)整線程池大小,您需要理解正在排隊的任務以及它們正在做什么。它們是 CPU 限制的(CPU-bound)嗎?它們是 I/O 限制的(I/O-bound)嗎?您的答案將影響您如何調(diào)整應用程序。
??調(diào)整線程池的大小基本上就是避免兩類錯誤:線程太少或線程太多。幸運的是,對于大多數(shù)應用程序來說,太多和太少之間的余地相當寬。
??一個系統(tǒng)最快的部分是CPU,所以決定一個系統(tǒng)吞吐量上限的是CPU。增強CPU處理能力,可以提高系統(tǒng)吞吐量上限。但根據(jù)短板效應,真實的系統(tǒng)吞吐量并不能單純根據(jù)CPU來計算。那要提高系統(tǒng)吞吐量,就需要從“系統(tǒng)短板”(比如網(wǎng)絡延遲、IO)著手:
- 盡量提高短板操作的并行化比率,比如多線程下載技術
- 增強短板能力,比如用NIO替代IO
CPU密集型應用
??CPU密集則是大量CPU時間都用于進行計算。需要進行矩陣運算視頻解碼這些操作的通常屬于CPU密集。觀察CPU占用的話多數(shù)時間都是出于I/O wait狀態(tài)(圖中綠色或黃色)。
I/O密集型應用
??一個I/O密集的應用通常行為是反復去讀寫磁盤文件(圖中藍色)。

通常,線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
??一般說來,大家認為線程池的大小經(jīng)驗值應該這樣設置:(其中N為CPU的個數(shù))
- 如果是CPU密集型應用,則線程池大小設置為N+1
- 如果是IO密集型應用,則線程池大小設置為2N+1
如果一臺服務器上只部署這一個應用并且只有這一個線程池,那么這種估算或許合理,具體還需自行測試驗證。但是,IO優(yōu)化中,確定線程池的大小相對比較復雜,涉及到下游系統(tǒng)的響應時間,因為一個線程常常因為等待其他系統(tǒng)的響應而被阻塞。所以我們必須增加線程的數(shù)量以更好地利用CPU,所以這樣的估算公式可能更適合:
??最佳線程數(shù)目 = (線程等待時間與線程CPU時間之比 + 1) CPU數(shù)目*
很顯然,線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
六、使用線程池就一定比使用單線程高效?
答案是否定的,比如Redis就是單線程的,但它卻非常高效,基本操作都能達到十萬量級/s。從線程這個角度來看,部分原因在于:多線程帶來線程上下文切換開銷,單線程就沒有這種開銷。
??當然“Redis很快”更本質(zhì)的原因在于:Redis基本都是內(nèi)存操作,這種情況下單線程可以很高效地利用CPU。而多線程適用場景一般是:存在相當比例的IO和網(wǎng)絡操作。