線程池

一、線程池簡(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)由于過度消耗內(nèi)存或“切換過度”而導(dǎo)致系統(tǒng)資源不足。這就引入了線程池概念。
??線程池的核心思想就是:連接復(fù)用,通過建立一個(gè)數(shù)據(jù)庫連接池以及一套連接使用、分配、管理策略,使得該線程池中的連接可以得到高效、安全的復(fù)用,避免了數(shù)據(jù)庫連接頻繁建立、關(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í)行的問題。
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):線程池中線程,在沒有任務(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):用于存放沒有處理的任務(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è)新的線程來替代它。此線程池保證所有任務(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è)可緩存的線程池。如果線程池的大小超過了處理任務(wù)所需要的線程,那么就會(huì)回收部分空閑(60秒不執(zhí)行任務(wù))的線程,當(dāng)任務(wù)數(shù)增加時(shí),此線程池又可以智能的添加新線程來處理任務(wù)。此線程池不會(huì)對(duì)線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說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ù)就是一個(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”); 
  } 
}

代碼說明:
1.ExecutorService接口對(duì)象來執(zhí)行任務(wù),該對(duì)象有兩個(gè)方法可以執(zhí)行任務(wù)execute和submit。

  • execute這種方式提交沒有返回值,也就不能判斷是否執(zhí)行成功。
  • submit這種方式它會(huì)返回一個(gè)Future對(duì)象。通過future的get方法來獲取返回值,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ì)列來存儲(chǔ)待處理工作job,并把隊(duì)列作為構(gòu)造函數(shù)參數(shù),從而實(shí)現(xiàn)業(yè)務(wù)可以靈活的擴(kuò)展定制線程池的隊(duì)列。業(yè)務(wù)也可使用JDK自身同步阻塞隊(duì)列SynchronousQueue、有界隊(duì)列ArrayBlockingQueue、無界隊(duì)列LinkedBlockingQueue。

  • SynchronousQueue是無界的,也就是說他存數(shù)任務(wù)的能力是沒有限制的,但是由于該Queue本身的特性,在某次添加元素后必須等待其他線程取走后才能繼續(xù)添加。如果運(yùn)行的線程等于或多于 corePoolSize,則 Executor始終首選將請(qǐng)求加入隊(duì)列,而不添加新的線程;如果無法將請(qǐng)求加入隊(duì)列,則創(chuàng)建新的線程,除非創(chuàng)建此線程超出maximumPoolSize,在這種情況下,任務(wù)將被拒絕。
  • LinkedBlockingQueue創(chuàng)建的線程就不會(huì)超過 corePoolSize。如果運(yùn)行的線程少于 corePoolSize,則 Executor 始終首選添加新的線程,而不進(jìn)行排隊(duì)。如果運(yùn)行的線程等于或多于 corePoolSize,則 Executor 始終首選將請(qǐng)求加入隊(duì)列,而不添加新的線程。換句說,永遠(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)超過處理任務(wù)的時(shí)間,而且還不斷增加,不一會(huì)兒就爆了。
  • ArrayBlockingQueue這個(gè)是最為復(fù)雜的使用,所以JDK不推薦使用也有些道理。與上面的相比,最大的特點(diǎn)便是可以防止資源耗盡的情況發(fā)生。

三、線程池帶來的好處

  • 降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗;
  • 提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行;
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
  • 線程池可以應(yīng)對(duì)突然大爆發(fā)量的訪問,通過有限個(gè)固定線程為大量的操作服務(wù),減少創(chuàng)建和銷毀線程所需的時(shí)間。

四、使用線程池的風(fēng)險(xiǎn)

雖然線程池是構(gòu)建多線程應(yīng)用程序的強(qiáng)大機(jī)制,但使用它并不是沒有風(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ù)又無法執(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ì)引起資源匱乏問題,因?yàn)槌刂芯€程正在消耗一些資源,而這些資源可能會(huì)被其它任務(wù)更有效地利用。
??除了線程自身所使用的資源以外,服務(wù)請(qǐng)求時(shí)所做的工作可能需要其它資源,例如 JDBC 連接、套接字或文件。這些也都是有限資源,有太多的并發(fā)請(qǐng)求也可能引起失效。如果一個(gè) web 應(yīng)用接收到的請(qǐng)求數(shù)高于線程池大小,多出來的請(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),例如使用無須編寫您自己的池中討論的util.concurrent 包。

線程泄露
??各種類型的線程池中一個(gè)嚴(yán)重的風(fēng)險(xiǎn)是線程泄漏,當(dāng)從池中除去一個(gè)線程以執(zhí)行一項(xiàng)任務(wù),而在任務(wù)完成后該線程卻沒有返回池時(shí),會(huì)發(fā)生這種情況。發(fā)生線程泄漏的一種情形出現(xiàn)在任務(wù)拋出一個(gè) RuntimeException 或一個(gè) Error 時(shí)。如果池類沒有捕捉到它們,那么線程只會(huì)退出而線程池的大小將會(huì)永久減少一個(gè)。當(dāng)這種情況發(fā)生的次數(shù)足夠多時(shí),線程池最終就為空,而且系統(tǒng)將停止,因?yàn)闆]有可用的線程來處理任務(wù)。
??有些任務(wù)可能會(huì)永遠(yuǎn)等待某些資源或來自用戶的輸入,而這些資源又不能保證變得可用,用戶可能也已經(jīng)回家了,諸如此類的任務(wù)會(huì)永久停止,而這些停止的任務(wù)也會(huì)引起和線程泄漏同樣的問題。如果某個(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)用程序來說,太多和太少之間的余地相當(dāng)寬。
??一個(gè)系統(tǒng)最快的部分是CPU,所以決定一個(gè)系統(tǒng)吞吐量上限的是CPU。增強(qiáng)CPU處理能力,可以提高系統(tǒng)吞吐量上限。但根據(jù)短板效應(yīng),真實(shí)的系統(tǒng)吞吐量并不能單純根據(jù)CPU來計(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í)間所占比例越高,需要越少線程。
??一般說來,大家認(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á)到十萬量級(jí)/s。從線程這個(gè)角度來看,部分原因在于:多線程帶來線程上下文切換開銷,單線程就沒有這種開銷。
??當(dāng)然“Redis很快”更本質(zhì)的原因在于:Redis基本都是內(nèi)存操作,這種情況下單線程可以很高效地利用CPU。而多線程適用場(chǎng)景一般是:存在相當(dāng)比例的IO和網(wǎng)絡(luò)操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容