在我們的開發中“池”的概念并不罕見,有數據庫連接池、線程池、對象池、常量池等等。下面我們主要針對線程池來一步一步揭開線程池的面紗。
使用線程池的好處
1、降低資源消耗
可以重復利用已創建的線程降低線程創建和銷毀造成的消耗。
2、提高響應速度
當任務到達時,任務可以不需要等到線程創建就能立即執行。
3、提高線程的可管理性
線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控
線程池的工作原理
首先我們看下當一個新的任務提交到線程池之后,線程池是如何處理的
1、線程池判斷核心線程池里的線程是否都在執行任務。如果不是,則創建一個新的工作線程來執行任務。如果核心線程池里的線程都在執行任務,則執行第二步。
2、線程池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里進行等待。如果工作隊列滿了,則執行第三步
3、線程池判斷線程池的線程是否都處于工作狀態。如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務
線程池飽和策略
這里提到了線程池的飽和策略,那我們就簡單介紹下有哪些飽和策略:
AbortPolicy
為Java線程池默認的阻塞策略,不執行此任務,而且直接拋出一個運行時異常,切記ThreadPoolExecutor.execute需要try catch,否則程序會直接退出。
DiscardPolicy
直接拋棄,任務不執行,空方法
DiscardOldestPolicy
從隊列里面拋棄head的一個任務,并再次execute 此task。
CallerRunsPolicy
在調用execute的線程里面執行此command,會阻塞入口
用戶自定義拒絕策略(最常用)
實現RejectedExecutionHandler,并自己定義策略模式
下我們以ThreadPoolExecutor為例展示下線程池的工作流程圖
1、如果當前運行的線程少于corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
2、如果運行的線程等于或多于corePoolSize,則將任務加入BlockingQueue。
3、如果無法將任務加入BlockingQueue(隊列已滿),則在非corePool中創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。
4、如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,并調用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述步驟的總體設計思路,是為了在執行execute()方法時,盡可能地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之后(當前運行的線程數大于等于corePoolSize),幾乎所有的execute()方法調用都是執行步驟2,而步驟2不需要獲取全局鎖。
關鍵方法源碼分析
我們看看核心方法添加到線程池方法execute的源碼如下:
下面我們繼續看看addWorker是如何實現的:
addWorker之后是runWorker,第一次啟動會執行初始化傳進來的任務firstTask;然后會從workQueue中取任務執行,如果隊列為空則等待keepAliveTime這么長時間
我們看下getTask是如何執行的
下面我們看下processWorkerExit是如何工作的
tryTerminate
processWorkerExit方法中會嘗試調用tryTerminate來終止線程池。這個方法在任何可能導致線程池終止的動作后執行:比如減少wokerCount或SHUTDOWN狀態下從隊列中移除任務。
shutdown這個方法會將runState置為SHUTDOWN,會終止所有空閑的線程。shutdownNow方法將runState置為STOP。和shutdown方法的區別,這個方法會終止所有的線程。主要區別在于shutdown調用的是interruptIdleWorkers這個方法,而shutdownNow實際調用的是Worker類的interruptIfStarted方法:
他們的實現如下:
線程池的使用 線程池的創建
我們可以通過ThreadPoolExecutor來創建一個線程池
向線程池提交任務
可以使用兩個方法向線程池提交任務,分別為execute()和submit()方法。execute()方法用于提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。通過以下代碼可知execute()方法輸入的任務是一個Runnable類的實例。
submit()方法用于提交需要返回值的任務。線程池會返回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間后立即返回,這時候有可能任務沒有執行完。
關閉線程池
可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將線程池的狀態設置成STOP,然后嘗試停止所有的正在執行或暫停任務的線程,并返回等待執行任務的列表,而shutdown只是將線程池的狀態設置成SHUTDOWN狀態,然后中斷所有沒有正在執行任務的線程。
只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉后,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至于應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown方法來關閉線程池,如果任務不一定要執行完,則可以調用shutdownNow方法。
合理的配置線程池
要想合理地配置線程池,就必須首先分析任務特性,可以從以下幾個角度來分析。
1、任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
2、任務的優先級:高、中和低。
3、任務的執行時間:長、中和短。
4、任務的依賴性:是否依賴其他系統資源,如數據庫連接。
性質不同的任務可以用不同規模的線程池分開處理。CPU密集型任務應配置盡可能小的線程,如配置Ncpu+1個線程的線程池。由于IO密集型任務線程并不是一直在執行任務,則應配置盡可能多的線程,如2*Ncpu。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那么分解后執行的吞吐量將高于串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先執行
如果一直有優先級高的任務提交到隊列里,那么優先級低的任務可能永遠不能執行。執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先級隊列,讓執行時間短的任務先執行。依賴數據庫連接池的任務,因為線程提交SQL后需要等待數據庫返回結果,等待的時間越長,則CPU空閑時間就越長,那么線程數應該設置得越大,這樣才能更好地利用CPU。
建議使用有界隊列。有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點兒,比如幾千。有時候我們系統里后臺任務線程池的隊列和線程池全滿了,不斷拋出拋棄任務的異常,通過排查發現是數據庫出現了問題,導致執行SQL變得非常緩慢,因為后臺任務線程池里的任務全是需要向數據庫查詢和插入數據的,所以導致線程池里的工作線程全部阻塞,任務積壓在線程池里。如果當時我們設置成無界隊列,那么線程池的隊列就會越來越多,有可能會撐滿內存,導致整個系統不可用,而不只是后臺任務出現問題。當然,我們的系統所有的任務是用單獨的服務器部署的,我們使用不同規模的線程池完成不同類型的任務,但是出現這樣問題時也會影響到其他任務。
線程池的監控
如果在系統中大量使用線程池,則有必要對線程池進行監控,方便在出現問題時,可以根據線程池的使用狀況快速定位問題。可以通過線程池提供的參數進行監控,在監控線程池的時候可以使用以下屬性
taskCount:線程池需要執行的任務數量。
completedTaskCount:線程池在運行過程中已完成的任務數量,小于或等于taskCount。
largestPoolSize:線程池里曾經創建過的最大線程數量。通過這個數據可以知道線程池是否曾經滿過。如該數值等于線程池的最大大小,則表示線程池曾經滿過。
getPoolSize:線程池的線程數量。如果線程池不銷毀的話,線程池里的線程不會自動銷毀,所以這個大小只增不減。
getActiveCount:獲取活動的線程數。
通過擴展線程池進行監控。可以通過繼承線程池來自定義線程池,重寫線程池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行后和線程池關閉前執行一些代碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。
轉載請注明來自夕逆IT,本文標題:《盲盒交友平臺源碼搭建盲盒交友小程序盲盒系統源碼》

還沒有評論,來說兩句吧...