熟悉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 一起交流學習!