使用線程池與不使用線程池的差別
先來看一下使用線程池與不使用線程池的差別,第一段代碼是使用線程池的:
public static void main(String[] args) { long startTime = System.currentTimeMillis(); final List<Integer> l = new LinkedList<Integer>(); ThreadPoolExecutor tp = new ThreadPoolExecutor(100, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(20000)); final Random random = new Random(); for (int i = 0; i < 20000; i++) { tp.execute(new Runnable() { public void run() { l.add(random.nextInt()); } }); } tp.shutdown(); try { tp.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis() - startTime); System.out.println(l.size()); }
接着是不使用線程池的:
public static void main(String[] args) { long startTime = System.currentTimeMillis(); final List<Integer> l = new LinkedList<Integer>(); final Random random = new Random(); for (int i = 0; i < 20000; i++) { Thread thread = new Thread() { public void run() { l.add(random.nextInt()); } }; thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(System.currentTimeMillis() - startTime); System.out.println(l.size()); }
運行一下,我這里第一段代碼使用了線程池的時間是194ms,第二段代碼不使用線程池的時間是2043ms。這里默認的線程池中的線程數是100,如果把這個數量減小,雖然系統的處理數據能力變弱了,但是速度卻更快了。當然這個例子很簡單,但也足夠說明問題了。
線程池的作用
線程池的作用就2個:
1、減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務
2、可以根據系統的承受能力,調整線程池中工作線程的數據,防止因為消耗過多的內存導致服務器崩潰
使用線程池,要根據系統的環境情況,手動或自動設置線程數目。少了系統運行效率不高,多了系統擁擠、占用內存多。用線程池控制數量,其他線程排隊等候。一個任務執行完畢,再從隊列中取最前面的任務開始執行。若任務中沒有等待任務,線程池這一資源處於等待。當一個新任務需要運行,如果線程池中有等待的工作線程,就可以開始運行了,否則進入等待隊列。
線程池類結構
畫了一張圖表示線程池的類結構圖:
這張圖基本簡單代表了線程池類的結構:
1、最頂級的接口是Executor,不過Executor嚴格意義上來說並不是一個線程池而只是提供了一種任務如何運行的機制而已
2、ExecutorService才可以認為是真正的線程池接口,接口提供了管理線程池的方法
3、下面兩個分支,AbstractExecutorService分支就是普通的線程池分支,ScheduledExecutorService是用來創建定時任務的
ThreadPoolExecutor六個核心參數
這篇文章重點講的就是線程池ThreadPoolExecutor,開頭也演示過ThreadPoolExecutor的使用了。
下面來看一下ThreadPoolExecutor完整構造方法的簽名,簽名中包含了六個參數,是ThreadPoolExecutor的核心,對這些參數的理解、配置、調優對於使用好線程池是非常重要的。因此接下來需要逐一理解每個參數的具體作用。先看一下構造方法簽名:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
1、corePoolSize
核心池的大小。在創建了線程池之后,默認情況下,線程池中沒有任何線程,而是等待有任務到來才創建線程去執行任務。默認情況下,在創建了線程池之后,線程池鍾的線程數為0,當有任務到來后就會創建一個線程去執行任務
2、maximumPoolSize
池中允許的最大線程數,這個參數表示了線程池中最多能創建的線程數量,當任務數量比corePoolSize大時,任務添加到workQueue,當workQueue滿了,將繼續創建線程以處理任務,maximumPoolSize表示的就是wordQueue滿了,線程池中最多可以創建的線程數量
3、keepAliveTime
只有當線程池中的線程數大於corePoolSize時,這個參數才會起作用。當線程數大於corePoolSize時,終止前多余的空閑線程等待新任務的最長時間
4、unit
keepAliveTime時間單位
5、workQueue
存儲還沒來得及執行的任務
6、threadFactory
執行程序創建新線程時使用的工廠
7、handler
由於超出線程范圍和隊列容量而使執行被阻塞時所使用的處理程序
corePoolSize與maximumPoolSize舉例理解
上面的內容,其他應該都相對比較好理解,只有corePoolSize和maximumPoolSize需要多思考。這里要特別再舉例以四條規則解釋一下這兩個參數:
1、池中線程數小於corePoolSize,新任務都不排隊而是直接添加新線程
2、池中線程數大於等於corePoolSize,workQueue未滿,首選將新任務加入workQueue而不是添加新線程
3、池中線程數大於等於corePoolSize,workQueue已滿,但是線程數小於maximumPoolSize,添加新的線程來處理被添加的任務
4、池中線程數大於大於corePoolSize,workQueue已滿,並且線程數大於等於maximumPoolSize,新任務被拒絕,使用handler處理被拒絕的任務
ThreadPoolExecutor的使用很簡單,前面的代碼也寫過例子了。通過execute(Runnable command)方法來發起一個任務的執行,通過shutDown()方法來對已經提交的任務做一個有效的關閉。盡管線程池很好,但我們要注意JDK API的一段話:
強烈建議程序員使用較為方便的Executors工廠方法Executors.newCachedThreadPool()(無界線程池,可以進行線程自動回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和Executors.newSingleThreadExecutor()(單個后台線程),它們均為大多數使用場景預定義了設置。
所以,跳開對ThreadPoolExecutor的關注(還是那句話,有問題查詢JDK API),重點關注一下JDK推薦的Executors。
Executors
個人認為,線程池的重點不是ThreadPoolExecutor怎么用或者是Executors怎么用,而是在合適的場景下使用合適的線程池,所謂"合適的線程池"的意思就是,ThreadPoolExecutor的構造方法傳入不同的參數,構造出不同的線程池,以滿足使用的需要。
下面來看一下Executors為用戶提供的幾種線程池:
1、newSingleThreadExecutos() 單線程線程池
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
單線程線程池,那么線程池中運行的線程數肯定是1。workQueue選擇了無界的LinkedBlockingQueue,那么不管來多少任務都排隊,前面一個任務執行完畢,再執行隊列中的線程。從這個角度講,第二個參數maximumPoolSize是沒有意義的,因為maximumPoolSize描述的是排隊的任務多過workQueue的容量,線程池中最多只能容納maximumPoolSize個任務,現在workQueue是無界的,也就是說排隊的任務永遠不會多過workQueue的容量,那maximum其實設置多少都無所謂了
2、newFixedThreadPool(int nThreads) 固定大小線程池
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
固定大小的線程池和單線程的線程池異曲同工,無非是讓線程池中能運行的線程編程了手動指定的nThreads罷了。同樣,由於是選擇了LinkedBlockingQueue,因此其實第二個參數maximumPoolSize同樣也是無意義的
3、newCachedThreadPool() 無界線程池
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
無界線程池,意思是不管多少任務提交進來,都直接運行。無界線程池采用了SynchronousQueue,采用這個線程池就沒有workQueue容量一說了,只要添加進去的線程就會被拿去用。既然是無界線程池,那線程數肯定沒上限,所以以maximumPoolSize為主了,設置為一個近似的無限大Integer.MAX_VALUE。 另外注意一下,單線程線程池和固定大小線程池線程都不會進行自動回收的,也即是說保證提交進來的任務最終都會被處理,但至於什么時候處理,就要看處理能力了。但是無界線程池是設置了回收時間的,由於corePoolSize為0,所以只要60秒沒有被用到的線程都會被直接移除
談談workQueue
上面三種線程池都提到了一個概念,workQueue,也就是排隊策略。排隊策略描述的是,當前線程大於corePoolSize時,線程以什么樣的方式排隊等待被運行。
排隊有三種策略:直接提交、有界隊列、無界隊列。
談談后兩種,JDK使用了無界隊列LinkedBlockingQueue作為WorkQueue而不是有界隊列ArrayBlockingQueue,盡管后者可以對資源進行控制,但是個人認為,使用有界隊列相比無界隊列有三個缺點:
1、使用有界隊列,corePoolSize、maximumPoolSize兩個參數勢必要根據實際場景不斷調整以求達到一個最佳,這勢必給開發帶來極大的麻煩,必須經過大量的性能測試。所以干脆就使用無界隊列,任務永遠添加到隊列中,不會溢出,自然maximumPoolSize也沒什么用了,只需要根據系統處理能力調整corePoolSize就可以了
2、防止業務突刺。尤其是在Web應用中,某些時候突然大量請求的到來都是很正常的。這時候使用無界隊列,不管早晚,至少保證所有任務都能被處理到。但是使用有界隊列呢?那些超出maximumPoolSize的任務直接被丟掉了,處理地慢還可以忍受,但是任務直接就不處理了,這似乎有些糟糕
3、不僅僅是corePoolSize和maximumPoolSize需要相互調整,有界隊列的隊列大小和maximumPoolSize也需要相互折衷,這也是一塊比較難以控制和調整的方面
當然,最后還是那句話,就像Comparable和Comparator的對比、synchronized和ReentrantLock,再到這里的無界隊列和有界隊列的對比,看似都有一個的優點稍微突出一些,但是這絕不是鼓勵大家使用一個而不使用另一個,任何東西都需要根據實際情況來,當然在一開始的時候可以重點考慮那些看上去優點明顯一點的
四種拒絕策略
所謂拒絕策略之前也提到過了,任務太多,超過maximumPoolSize了怎么把?當然是接不下了,接不下那只有拒絕了。拒絕的時候可以指定拒絕策略,也就是一段處理程序。
決絕策略的父接口是RejectedExecutionHandler,JDK本身在ThreadPoolExecutor里給用戶提供了四種拒絕策略,看一下:
1、AbortPolicy
直接拋出一個RejectedExecutionException,這也是JDK默認的拒絕策略
2、CallerRunsPolicy
嘗試直接運行被拒絕的任務,如果線程池已經被關閉了,任務就被丟棄了
3、DiscardOldestPolicy
移除最晚的那個沒有被處理的任務,然后執行被拒絕的任務。同樣,如果線程池已經被關閉了,任務就被丟棄了
4、DiscardPolicy
不能執行的任務將被刪除