天天看點

Java多線程:徹底搞懂線程池

熟悉Java多線程程式設計的同學都知道,當我們線程建立過多時,容易引發記憶體溢出,是以我們就有必要使用線程池的技術了。 最近看了一些相關文章,并親自研究了一下源碼,發現有些文章還是有些問題的,是以我也總結了一下,在此奉獻給大家。

目錄

1 線程池的優勢

總體來說,線程池有如下的優勢:

(1)降低資源消耗。通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。

(2)提高響應速度。當任務到達時,任務可以不需要等到線程建立就能立即執行。

(3)提高線程的可管理性。線程是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的配置設定,調優和監控。

2 線程池的使用

線程池的真正實作類是ThreadPoolExecutor,其構造方法有如下4種:

可以看到,其需要如下幾個參數:

corePoolSize(必需):核心線程數。預設情況下,核心線程會一直存活,但是當将allowCoreThreadTimeout設定為true時,核心線程也會逾時回收。

maximumPoolSize(必需):線程池所能容納的最大線程數。當活躍線程數達到該數值後,後續的新任務将會阻塞。

keepAliveTime(必需):線程閑置逾時時長。如果超過該時長,非核心線程就會被回收。如果将allowCoreThreadTimeout設定為true時,核心線程也會逾時回收。

unit(必需):指定keepAliveTime參數的時間機關。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

workQueue(必需):任務隊列。通過線程池的execute()方法送出的Runnable對象将存儲在該參數中。其采用阻塞隊列實作。

threadFactory(可選):線程工廠。用于指定為線程池建立新線程的方式。

handler(可選):拒絕政策。當達到最大線程數時需要執行的飽和政策。

線程池的使用流程如下:

3 線程池的工作原理

下面來描述一下線程池工作的原理,同時對上面的參數有一個更深的了解。其工作原理流程圖如下:

工作原理

通過上圖,相信大家已經對所有參數有個了解了。其實還有一點,線上程池中并沒有區分線程是否是核心線程的。下面我們再對任務隊列、線程工廠和拒絕政策做更多的說明。

4 線程池的參數

任務隊列是基于阻塞隊列實作的,即采用生産者消費者模式,在Java中需要實作BlockingQueue接口。但Java已經為我們提供了7種阻塞隊列的實作:

ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列(數組結構可配合指針實作一個環形隊列)。

LinkedBlockingQueue: 一個由連結清單結構組成的有界阻塞隊列,在未指明容量時,容量預設為Integer.MAX_VALUE。

PriorityBlockingQueue: 一個支援優先級排序的無界阻塞隊列,對元素沒有要求,可以實作Comparable接口也可以提供Comparator來對隊列中的元素進行比較。跟時間沒有任何關系,僅僅是按照優先級取任務。

DelayQueue:類似于PriorityBlockingQueue,是二叉堆實作的無界優先級阻塞隊列。要求元素都實作Delayed接口,通過執行時延從隊列中提取任務,時間沒到任務取不出來。

SynchronousQueue: 一個不存儲元素的阻塞隊列,消費者線程調用take()方法的時候就會發生阻塞,直到有一個生産者線程生産了一個元素,消費者線程就可以拿到這個元素并傳回;生産者線程調用put()方法的時候也會發生阻塞,直到有一個消費者線程消費了一個元素,生産者才會傳回。

LinkedBlockingDeque: 使用雙向隊列實作的有界雙端阻塞隊列。雙端意味着可以像普通隊列一樣FIFO(先進先出),也可以像棧一樣FILO(先進後出)。

LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的結合體,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行為一緻,但是是無界的阻塞隊列。

注意有界隊列和無界隊列的差別:如果使用有界隊列,當隊列飽和時并超過最大線程數時就會執行拒絕政策;而如果使用無界隊列,因為任務隊列永遠都可以添加任務,是以設定maximumPoolSize沒有任何意義。

線程工廠指定建立線程的方式,需要實作ThreadFactory接口,并實作newThread(Runnable r)方法。該參數可以不用指定,Executors架構已經為我們實作了一個預設的線程工廠:

當線程池的線程數達到最大線程數時,需要執行拒絕政策。拒絕政策需要實作RejectedExecutionHandler接口,并實作rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不過Executors架構已經為我們實作了4種拒絕政策:

AbortPolicy(預設):丢棄任務并抛出RejectedExecutionException異常。

CallerRunsPolicy:由調用線程處理該任務。

DiscardPolicy:丢棄任務,但是不抛出異常。可以配合這種模式進行自定義的處理方式。

DiscardOldestPolicy:丢棄隊列最早的未處理任務,然後重新嘗試執行任務。

5 功能線程池

嫌上面使用線程池的方法太麻煩?其實Executors已經為我們封裝好了4種常見的功能線程池,如下:

定長線程池(FixedThreadPool)

定時線程池(ScheduledThreadPool )

可緩存線程池(CachedThreadPool)

單線程化線程池(SingleThreadExecutor)

建立方法的源碼:

特點:隻有核心線程,線程數量固定,執行完立即回收,任務隊列為連結清單結構的有界隊列。

應用場景:控制線程最大并發數。

使用示例:

特點:核心線程數量固定,非核心線程數量無限,執行完閑置10ms後回收,任務隊列為延時阻塞隊列。

應用場景:執行定時或周期性的任務。

特點:無核心線程,非核心線程數量無限,執行完閑置60s後回收,任務隊列為不存儲元素的阻塞隊列。

應用場景:執行大量、耗時少的任務。

特點:隻有1個核心線程,無非核心線程,執行完立即回收,任務隊列為連結清單結構的有界隊列。

應用場景:不适合并發但可能引起IO阻塞性及影響UI線程響應的操作,如資料庫操作、檔案操作等。

對比 - 引自Carson_Ho

6 總結

Executors的4個功能線程池雖然友善,但現在已經不建議使用了,而是建議直接通過使用ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明确線程池的運作規則,規避資源耗盡的風險。

其實Executors的4個功能線程有如下弊端:

FixedThreadPool和SingleThreadExecutor:主要問題是堆積的請求處理隊列均采用LinkedBlockingQueue,可能會耗費非常大的記憶體,甚至OOM。

CachedThreadPool和ScheduledThreadPool:主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量非常多的線程,甚至OOM。

參考

Android多線程:線程池ThreadPool 全面解析

還在用Executors建立線程池?小心記憶體溢出

《阿裡巴巴java開發手冊》

最後,歡迎加我微信 jimmysun8388 一起交流學習!

繼續閱讀