△ 公眾號回復關鍵詞「架構」 即可領取《1500+BAT架構及面試專題合集》

本篇為線程池系列文章之一,不經常使用線程池的童鞋,還有對幾種線程的使用不甚了解的童鞋,可以讀一下此文,並關注後續線程池相關文章連載。

本篇內容大綱:

  1. 線程池的由來
  2. 線程池的優點和風險
  3. 線程池的原理和實現
  4. 線程池大小的配置
  5. 線程池的四種實現

一、線程池的由來

我們有兩種常見的創建線程的方法,一種是繼承Thread類,一種是實現Runnable的介面,Thread類其實也是實現了Runnable介面。但是我們創建這兩種線程在運行結束後都會被虛擬機銷毀,如果線程數量多的話,頻繁的創建和銷毀線程會大大浪費時間和效率,更重要的是浪費內存。那麼有沒有一種方法能讓線程運行完後不立即銷毀,而是讓線程重複使用,繼續執行其他的任務哪?

這就是線程池的由來,很好的解決線程的重複利用,避免重複開銷。

二、線程池的優點

1、線程是稀缺資源,使用線程池可以減少創建和銷毀線程的次數,每個工作線程都可以重複使用。

2、可以根據系統的承受能力,調整線程池中工作線程的數量,防止因為消耗過多內存導致伺服器崩潰。

三、線程池的風險

雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有並發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。

1.死鎖

任何多線程應用程序都有死鎖風險。當一組進程或線程中的每一個都在等待一個只有該組中另一個進程才能引起的事件時,我們就說這組進程或線程 死鎖了。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線程將永遠等下去。

2.資源不足

線程池的一個優點在於:相對於其它替代調度機制(有些我們已經討論過)而言,它們通常執行得很好。但只有恰當地調整了線程池大小時才是這樣的。

線程消耗包括內存和其它系統資源在內的大量資源

除了 Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會為每個 Java 線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。

如果線程池太大,那麼被那些線程消耗的資源可能嚴重地影響系統性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏問題,因為池線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。

3.並發錯誤

線程池和其它排隊機制依靠使用 wait() 和 notify() 方法,這兩個方法都難於使用。如果編碼不正確,那麼可能丟失通知,導致線程保持空閑狀態,儘管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現有的、已經知道能工作的實現,例如在 util.concurrent 包。

4.線程泄漏

各種類型的線程池中一個嚴重的風險是線程泄漏,當從池中除去一個線程以執行一項任務,而在任務完成後該線程卻沒有返回池時,會發生這種情況。發生線程泄漏的一種情形出現在任務拋出一個 RuntimeException 或一個 Error 時。

如果池類沒有捕捉到它們,那麼線程只會退出而線程池的大小將會永久減少一個。當這種情況發生的次數足夠多時,線程池最終就為空,而且系統將停止,因為沒有可用的線程來處理任務。

5.請求過載

僅僅是請求就壓垮了伺服器,這種情況是可能的。在這種情形下,我們可能不想將每個到來的請求都排隊到我們的工作隊列,因為排在隊列中等待執行的任務可能會消耗太多的系統資源並引起資源缺乏。在這種情形下決定如何做取決於您自己;在某些情況下,您可以簡單地拋棄請求,依靠更高級別的協議稍後重試請求,您也可以用一個指出伺服器暫時很忙的響應來拒絕請求。

四、線程池的實現原理

1.線程池狀態

線程池和線程一樣擁有自己的狀態,在ThreadPoolExecutor類中定義了一個volatile變數runState來表示線程池的狀態,線程池有四種狀態,分別為RUNNING、SHURDOWN、STOP、TERMINATED。

1)線程池創建後處於RUNNING狀態。

2)調用shutdown後處於SHUTDOWN狀態,線程池不能接受新的任務,會等待緩衝隊列的任務完成。

3)調用shutdownNow後處於STOP狀態,線程池不能接受新的任務,並嘗試終止正在執行的任務。

4)當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷毀,任務緩存隊列已經清空或執行結束後,線程池被設置為TERMINATED狀態。

線程池原理:預先啟動一些線程,線程無限循環從任務隊列中獲取一個任務進行執行,直到線程池被關閉。如果某個線程因為執行某個任務發生異常而終止,那麼重新創建一個新的線程而已,如此反覆。

2.線程池的處理流程

1)判斷線程池裡的核心線程是否都在執行任務,如果不是(核心線程空閑或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。

2)線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。

3)判斷線程池裡的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

五、線程池的配置大小

一般需要根據任務的類型來配置線程池大小:

  1. 如果是CPU密集型任務,就需要盡量壓榨CPU,參考值可以設為 NCPU+1
  2. 如果是IO密集型任務,參考值可以設置為2*NCPU

當然,這只是一個參考值,具體的設置還需要根據實際情況進行調整,比如可以先將線程池大小設置為參考值,再觀察任務運行情況和系統負載、資源利用率來進行適當調整。

六、Java線程池的四種實現

  1. newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
  2. newFixedThreadPool 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
  3. newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
  4. newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

-end-

關於線程池,歡迎評論區留言交流。

如果覺得不錯,感謝點贊支持下~

△ 公眾號回復關鍵詞「架構」 即可領取《1500+BAT架構及面試專題合集》

推薦閱讀:
相关文章