##引言## 合理利用線程池可以帶來三個好處。**第一:下降資源消耗。**經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。**第二:提升響應速度。**當任務到達時,任務能夠不須要的等到線程建立就能當即執行。**第三:提升線程的可管理性。**線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。可是要作到合理的利用線程池,必須對其原理了如指掌。 ##線程池的使用## ###線程池的建立### 咱們能夠經過ThreadPoolExecutor來建立一個線程池。java
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
建立一個線程池須要輸入幾個參數: corePoolSize(線程池的基本大小)
:當提交一個任務到線程池時,線程池會建立一個線程來執行任務,即便其餘空閒的基本線程可以執行新任務也會建立線程,等到須要執行的任務數大於線程池基本大小時就再也不建立。若是調用了線程池的prestartAllCoreThreads方法,線程池會提早建立並啓動全部基本線程。 runnableTaskQueue(任務隊列)
:用於保存等待執行的任務的阻塞隊列。能夠選擇如下幾個阻塞隊列。 (1)ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。 (2)LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。 (3)SynchronousQueue:一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。 (4)PriorityBlockingQueue:一個具備優先級得無限阻塞隊列。 maximumPoolSize(線程池最大大小)
:線程池容許建立的最大線程數。若是隊列滿了,而且已建立的線程數小於最大線程數,則線程池會再建立新的線程執行任務。值得注意的是若是使用了無界的任務隊列這個參數就沒什麼效果。 ThreadFactory
:用於設置建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程設置更有意義的名字,Debug和定位問題時很是又幫助。 RejectedExecutionHandler(飽和策略)
:當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採起一種策略處理提交的新任務。這個策略默認狀況下是AbortPolicy,表示沒法處理新任務時拋出異常。如下是JDK1.5提供的四種策略。 (1)AbortPolicy:直接拋出異常。 (2)CallerRunsPolicy:只用調用者所在線程來運行任務。 (3)DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。 (4)DiscardPolicy:不處理,丟棄掉。 (5)固然也能夠根據應用場景須要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。 keepAliveTime(線程活動保持時間)
:線程池的工做線程空閒後,保持存活的時間。因此若是任務不少,而且每一個任務執行的時間比較短,能夠調大這個時間,提升線程的利用率。 TimeUnit(線程活動保持時間的單位)
:可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。 ###向線程池提交任務### 咱們可使用execute提交的任務,可是execute方法沒有返回值,因此沒法判斷任務知否被線程池執行成功。經過如下代碼可知execute方法輸入的任務是一個Runnable類的實例。數據庫
threadsPool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } });
咱們也可使用submit 方法來提交任務,它會返回一個future,那麼咱們能夠經過這個future來判斷任務是否執行成功,經過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後當即返回,這時有可能任務沒有執行完。數組
try { Object s = future.get(); } catch (InterruptedException e) { // 處理中斷異常 } catch (ExecutionException e) { // 處理沒法執行任務異常 } finally { // 關閉線程池 executor.shutdown(); }
###線程池的關閉### 咱們能夠經過調用線程池的shutdown或shutdownNow方法來關閉線程池,可是它們的實現原理不一樣,shutdown的原理是隻是將線程池的狀態設置成SHUTDOWN狀態,而後中斷全部沒有正在執行任務的線程。shutdownNow的原理是遍歷線程池中的工做線程,而後逐個調用線程的interrupt方法來中斷線程,因此沒法響應中斷的任務可能永遠沒法終止。shutdownNow會首先將線程池的狀態設置成STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表。服務器
只要調用了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當全部的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於咱們應該調用哪種方法來關閉線程池,應該由提交到線程池的任務特性決定,一般調用shutdown來關閉線程池,若是任務不必定要執行完,則能夠調用shutdownNow。 ##線程池的分析## 流程分析:線程池的主要工做流程以下圖: ![在此輸入圖片描述][1] 從上圖咱們能夠看出,當提交一個新任務到線程池時,線程池的處理流程以下: (1)首先線程池判斷基本線程池是否已滿?沒滿,建立一個工做線程來執行任務。滿了,則進入下個流程。 (2)其次線程池判斷工做隊列是否已滿?沒滿,則將新提交的任務存儲在工做隊列裏。滿了,則進入下個流程。 (3)最後線程池判斷整個線程池是否已滿?沒滿,則建立一個新的工做線程來執行任務,滿了,則交給飽和策略來處理這個任務。ide
上面的流程分析讓咱們很直觀的瞭解的線程池的工做原理,讓咱們再經過源代碼來看看是如何實現的。線程池執行任務的方法以下:this
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); //若是線程數小於基本線程數,則建立線程並執行當前任務 if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { //如線程數大於等於基本線程數或線程建立失敗,則將當前任務放到工做隊列中。 if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } //若是線程池不處於運行中或任務沒法放入隊列,而且當前線程數量小於最大容許的線程數量,則建立一個線程執行任務。 else if (!addIfUnderMaximumPoolSize(command)) //拋出RejectedExecutionException異常 reject(command); // is shutdown or saturated } }
線程池建立線程時,會將線程封裝成工做線程Worker,Worker在執行完任務後,還會無限循環獲取工做隊列裏的任務來執行。咱們能夠從Worker的run方法裏看到這點:spa
public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } }
##合理的配置線程池## 要想合理的配置線程池,就必須首先分析任務特性,能夠從如下幾個角度來進行分析: (1)任務的性質:CPU密集型任務,IO密集型任務和混合型任務。 (2)任務的優先級:高,中和低。 (3)任務的執行時間:長,中和短。 (4)任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接。 任務性質不一樣的任務能夠用不一樣規模的線程池分開處理。CPU密集型任務配置儘量少的線程數量,如配置Ncpu+1個線程的線程池。IO密集型任務則因爲須要等待IO操做,線程並非一直在執行任務,則配置儘量多的線程,如2*Ncpu。混合型的任務,若是能夠拆分,則將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,若是這兩個任務執行時間相差太大,則不必進行分解。咱們能夠經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數。.net
優先級不一樣的任務可使用優先級隊列PriorityBlockingQueue來處理。它可讓優先級高的任務先獲得執行,須要注意的是若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。線程
執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者也可使用優先級隊列,讓執行時間短的任務先執行。rest
依賴數據庫鏈接池的任務,由於線程提交SQL後須要等待數據庫返回結果,若是等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。
建議使用有界隊列,有界隊列能增長系統的穩定性和預警能力,能夠根據須要設大一點,好比幾千。有一次咱們組使用的後臺任務線程池的隊列和線程池全滿了,不斷的拋出拋棄任務的異常,經過排查發現是數據庫出現了問題,致使執行SQL變得很是緩慢,由於後臺任務線程池裏的任務全是須要向數據庫查詢和插入數據的,因此致使線程池裏的工做線程所有阻塞住,任務積壓在線程池裏。若是當時咱們設置成無界隊列,線程池的隊列就會愈來愈多,有可能會撐滿內存,致使整個系統不可用,而不僅是後臺任務出現問題。固然咱們的系統全部的任務是用的單獨的服務器部署的,而咱們使用不一樣規模的線程池跑不一樣類型的任務,可是出現這樣問題時也會影響到其餘任務。
##線程池的監控## 經過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候可使用。 taskCount
:線程池須要執行的任務數量。 completedTaskCount
:線程池在運行過程當中已完成的任務數量。小於或等於taskCount。 largestPoolSize
:線程池曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。 getPoolSize
:線程池的線程數量。若是線程池不銷燬的話,池裏的線程不會自動銷燬,因此這個大小隻增不減。 getActiveCount
:獲取活動的線程數。
經過擴展線程池進行監控。經過繼承線程池並重寫線程池的beforeExecute,afterExecute和terminated方法,咱們能夠在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。 [1]: http://static.oschina.net/uploads/space/2015/0318/171952_gf1I_120166.jpg