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

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

線程池的緣由

java中為了提高並發度,可以使用多線程

共同執行,但是如果有大量線程短時間之內被創建和銷毀,會佔用大量的系統時間,影響系統效率。

為了解決上面的問題,java中引入了線程池,可以使創建好的線程在指定的時間內由系統統一管理,而不是在執行時創建,執行後就銷毀,從而避免了頻繁創建、銷毀線程帶來的系統開銷。

線程池如何使用,以及實現原理,有什麼使用注意事項等,今天主要從這幾個方面詳細介紹Java線程池。

線程池的實現原理

當系統接受一個提交的任務時,並不會著急去創建一個新的線程去執行這個任務,而是去線程池中查詢是否有空閑的線程。

  • 若有:直接使用這個線程。
  • 若沒有:根據配置的策略執行(有可能時創建一個新的線程,也有可能是阻塞該任務等待空閑線程)。
  • 待任務結束之後,也不會銷毀線程,而是放入線程池的空閑隊列,等待下次使用。

就以ThreadPoolExecutor為例,當我們把一個Runnable交給線程池去執行的時候,這個線程池處理的流程是這樣的:

  1. 先判斷線程池中的核心線程們是否空閑,如果空閑,就把這個新的任務指派給某一個空閑線程去執行。如果沒有空閑,並且當前線程池中的核心線程數還小於 corePoolSize,那就再創建一個核心線程。
  2. 如果線程池的線程數已經達到核心線程數,並且這些線程都繁忙,就把這個新來的任務放到等待隊列中去。如果等待隊列又滿了,那麼查看一下當前線程數是否到達maximumPoolSize,如果還未到達,就繼續創建線程。
  3. 如果已經到達了,就交給RejectedExecutionHandler(拒絕策略)來決定怎麼處理這個任務。

線程池的使用(ThreadPoolExecutor)

在Java中,線程池的概念是Executor這個介面,具體實現為ThreadPoolExecutor類,java.uitl.concurrent.ThreadPoolExecutor是線程池中最核心的一個類,因此如果要透徹地了解Java中的線程池,必須先了解這個類。

ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器

public class ThreadPoolExecutor extends AbstractExecutorService {

.....public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

...

}

ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作。

下面解釋下一下構造器中各個參數的含義:

1.corePoolSize(線程池的基本大小)

當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啟動所有基本線程。

2.runnableTaskQueue(任務隊列)

用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列。

  • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
  • LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
  • PriorityBlockingQueue:一個具有優先順序得無限阻塞隊列。

3.maximumPoolSize(線程池最大大小)

線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。

4.ThreadFactory:用於設置創建線程的工廠

可以通過線程工廠給每個創建出來的線程設置更有意義的名字,Debug和定位問題時非常又幫助。

5.RejectedExecutionHandler(飽和策略)

當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。以下是JDK1.5提供的四種策略。n AbortPolicy:直接拋出異常。

  • CallerRunsPolicy:只用調用者所在線程來運行任務。
  • DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務。
  • DiscardPolicy:不處理,丟棄掉。
  • 當然也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化不能處理的任務。

6.keepAliveTime(線程活動保持時間)

線程池的工作線程空閑後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。

7.TimeUnit(線程活動保持時間的單位)

可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

線程池的注意事項

雖然線程池能大大提高伺服器的並發性能,但使用它也會存在一定風險。與所有多線程應用程序一樣,用線程池構建的應用程序容易產生各種並發問題,如對共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理地使用線程池,還容易導致與線程池有關的死鎖、系統資源不足和線程泄漏等問題。

1) 建議使用new ThreadPoolExecutor(...)的方式創建線程池

線程池的創建不應使用 Executors 去創建,而應該通過 ThreadPoolExecutor 創建,這樣可以讓讀者更加明確地知道線程池的參數設置、運行規則,規避資源耗盡的風險,這一點在也阿里巴巴JAVA開發手冊中也有明確要求。這一點不容小覷,曾有同學因為線程池使用不當導致生產的同一台機器上部署的多個應用都因無法創建線程池而出現故障。

2) 合理設置線程數

線程池的工作線程數設置應根據實際情況配置,CPU密集型業務(搜索、排序等)CPU空閑時間較少,線程數不能設置太多。

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

如果是IO密集型任務,參考值可以設置為2*NCPU

3) 設置能代表具體業務的線程名稱

這樣方便通過日誌的線程名稱識別所屬業務。具體實現可以通過指定ThreadPoolExecutor的ThreadFactory參數。如使Spring提供的CustomizableThreadFactory。

以上就是Java線程池的詳細介紹,除了從編程的角度應對高並發,更多還需要從架構設計的層面來應對高並發場景,例如:Redis緩存、MySQL資料庫的優化、非同步消息等,詳細的內容如下。

-end-

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

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

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


推薦閱讀:
相关文章