創建和銷毀線程非常損耗性能,那有沒有可能復用一些已經被創建好的線程呢?答案是肯定的,那就是線程池。
另外,線程的創建需要開辟虛擬機棧、本地方法棧、程序計數器等線程私有的內存空間,在線程銷毀時需要回收這些系統資源,頻繁地創建銷毀線程會浪費大量資源,而通過復用已有線程可以更好地管理和協調線程的工作。
線程池主要解決兩個問題:
1、當執行大量異步任務時線程池能夠提供很好的性能。
2、線程池提供了一種資源限制和管理的手段,比如可以限制線程的個數,動態新增線程。
創建線程池
為了更方便的使用線程池,JDK 中給我們提供了一個線程池的工廠類Executors。在 Executors 中定義了多個靜態方法,用來創建不同配置的線程池。常見有以下幾種:
newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按先進先出的順序執行。

執行上述代碼結果如下,可以看出所有的 task 始終是在同一個線程中被執行的。

newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。


執行效果如下:

從上面日志中可以看出,緩存線程池會創建新的線程來執行任務。但是如果將代碼修改一下,在提交任務之前休眠 1 秒鍾,如下:

再次執行則打印日志同 SingleThreadPool 一模一樣,原因是提交的任務只需要 500 毫秒即可執行完畢,休眠 1 秒導致在新的任務提交之前,線程 “pool-1-thread-1” 已經處於空閑狀態,可以被復用執行任務。
newFixedThreadPool
創建一個固定數目的、可重用的線程池。

上述代碼創建了一個固定數量 3 的線程池,因此雖然向線程池提交了 10 個任務,但是這 10 個任務只會被 3 個線程分配執行,執行效果如下:

newScheduledThreadPool
創建一個定時線程池,支持定時及周期性任務執行。

上面代碼創建了一個線程數量為 2 的定時任務線程池,通過 scheduleAtFixedRate 方法,指定每隔 500 毫秒執行一次任務,並且在 5 秒鍾之后通過 shutdown 方法關閉定時任務。執行效果如下:

上面這幾種就是常用到的線程池使用方式,但是!在阿里Java開發手冊中已經嚴禁使用 Executors 來創建線程池,這是為什么?要回答這個問題需要先了解線程池的工作原理。
線程池工作原理分析
線程池的構造器如下:


- corePoolSize:表示核心線程數量
- maximumPoolSize:表示線程池最大能夠容納同時執行的線程數,必須大於或等於 1。如果和 corePoolSize 相等即是固定大小線程池
- keepAliveTime:表示線程池中的線程空閑時間,當空閑時間達到此值時,線程會被銷毀直到剩下 corePoolSize 個線程
- unit:用來指定 keepAliveTime 的時間單位,有 MILLISECONDS、SECONDS、MINUTES、HOURS 等
- workQueue:等待隊列,BlockingQueue 類型。當請求任務數大於 corePoolSize 時,任務將被緩存在此 BlockingQueue 中
- threadFactory:線程工廠,線程池中使用它來創建線程,如果傳入的是 null,則使用默認工廠類 DefaultThreadFactory
- handler:執行拒絕策略的對象。當 workQueue 滿了之后並且活動線程數大於 maximumPoolSize 的時候,線程池通過該策略處理請求
需要注意的是當ThreadPoolExecutor 的 allowCoreThreadTimeOut 設置為 true 時,核心線程超時后也會被銷毀。
流程解析
當我們調用 execute 或者 submit,將一個任務提交給線程池,線程池收到這個任務請求后,有以下幾種處理情況:
1、當前線程池中運行的線程數量還沒有達到 corePoolSize 大小時,線程池會創建一個新線程執行提交的任務,無論之前創建的線程是否處於空閑狀態。

上面代碼創建了 3 個固定數量的線程池,每次提交的任務耗時 100 毫秒。每次提交任務之前都會延遲2秒,保證線程池中的工作線程都已經執行完畢,但是執行效果如下:

可以看出雖然線程 1 和線程 2 都已執行完畢並且處於空閑狀態,但是線程池還是會嘗試創建新的線程去執行新提交的任務,直到線程數量達到 corePoolSize。
2、當前線程池中運行的線程數量已經達到 corePoolSize 大小時,線程池會把任務加入到等待隊列中,直到某一個線程空閑了,線程池會根據我們設置的等待隊列規則,從隊列中取出一個新的任務執行。

上述代碼提交的任務耗時 4 秒,因此前 2 個任務會占用線程池中的 2 個核心線程。此時有新的任務提交給線程池時,任務會被緩存到等待隊列中,結果如下:

可以看到紅框 1 中通過 2 個核心線程直接執行提交的任務,因此等待隊列中的數量為 0;而紅框 2 中表明,此時核心線程都已經被占用,新提交的任務都被放入等待隊列中。
3、如果線程數大於 corePoolSize 數量但是還沒有達到最大線程數 maximumPoolSize,並且等待隊列已滿,則線程池會創建新的線程來執行任務。

上述代碼創建了一個核心線程數為 2,最大線程數為 10,等待隊列長度為 2 的線程池。執行效果如下:

解釋說明:
- 1 處表示線程數量已經達到 corePoolSize
- 2 處表明等待隊列已滿
- 3 處會創建新的線程執行任務
4、最后如果提交的任務,無法被核心線程直接執行,又無法加入等待隊列,又無法創建“非核心線程”直接執行,線程池將根據拒絕處理器定義的策略處理這個任務。比如在 ThreadPoolExecutor 中,如果你沒有為線程池設置 RejectedExecutionHandler。這時線程池會拋出 RejectedExecutionException 異常,即線程池拒絕接受這個任務。

修改最大線程數為 3,並提交 6 次任務給線程池,執行效果如下:

程序會報異常 RejectedExecutionException,拒絕策略是線程池的一種保護機制,目的就是當這種無節制的線程資源申請發生時,拒絕新的任務保護線程池。

為何禁止使用 Executors
現在再回頭看一下為何在阿里 Java 開發手冊中嚴禁使用 Executors 工具類來創建線程池。尤其是 newFixedThreadPool 和 newCachedThreadPool 這兩個方法。
比如如下使用 newFixedThreadPool 方法創建線程的案例:

上述代碼創建了一個固定數量為 2 的線程池,並通過 for 循環向線程池中提交 100 萬個任務。通過 java -Xms4m -Xmx4m FixedThreadPoolOOM 執行上述代碼:

可以發現當任務添加到 7 萬多個時,程序發生 OOM。 看一下newSingleThreadExecutor 和 newFixedThreadPool() 的具體實現,如下:

可以看到傳入的是一個無界的阻塞隊列,理論上可以無限添加任務到線程池。當核心線程執行時間很長(比如 sleep10s),則新提交的任務還在不斷地插入到阻塞隊列中,最終造成
OOM。
再看下 newCachedThreadPool 會有什么問題。

同樣會報 OOM,只是錯誤的 log 信息有點區別:無法創建新的線程。

看一下 newCachedThreadPool 的實現:

可以看到,緩存線程池的最大線程數為 Integer 最大值。當核心線程耗時很久,線程池會嘗試創建新的線程來執行提交的任務,當內存不足時就會報無法創建線程的錯誤。
總結
線程池是一把雙刃劍,使用得當會使代碼如虎添翼;但是使用不當將會造成重大性災難。而劍柄是握在開發者手中,只有理解線程池的運行原理,熟知它的工作機制與使用場景,才會使這把雙刃劍發揮更好的作用。
