如何優(yōu)雅的自定義 ThreadPoolExecutor 線程池?
點擊關注公眾號,Java干貨及時送達??

1、概述
java 中經常需要用到多線程來處理一些業(yè)務,非常不建議單純使用繼承Thread或者實現(xiàn)Runnable接口的方式來創(chuàng)建線程,那樣勢必有創(chuàng)建及銷毀線程耗費資源、線程上下文切換問題。同時創(chuàng)建過多的線程也可能引發(fā)資源耗盡的風險,這個時候引入線程池比較合理,方便線程任務的管理。
java中涉及到線程池的相關類均在 jdk 1.5 開始的java.util.concurrent包中,涉及到的幾個核心類及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
JDK 自動創(chuàng)建線程池的幾種方式都封裝在Executors工具類中:
newFixedThreadPool
使用的構造方式為
new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())
設置了corePoolSize=maxPoolSize,keepAliveTime=0(此時該參數(shù)沒作用),無界隊列,任務可以無限放入,當請求過多時(任務處理速度跟不上任務提交速度造成請求堆積)可能導致占用過多內存或直接導致OOM異常。
newSingleThreadExector
使用的構造方式為
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)
基本同 newFixedThreadPool,但是將線程數(shù)設置為了1,單線程,弊端和newFixedThreadPool 一致。
newCachedThreadPool
使用的構造方式為
new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())
corePoolSize=0,maxPoolSize為很大的數(shù),同步移交隊列,也就是說不維護常駐線程(核心線程),每次來請求直接創(chuàng)建新線程來處理任務,也不使用隊列緩沖,會自動回收多余線程,由于將maxPoolSize設置成Integer.MAX_VALUE,當請求很多時就可能創(chuàng)建過多的線程,導致資源耗盡OOM。
newScheduledThreadPool
使用的構造方式為
new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue())
支持定時周期性執(zhí)行,注意一下使用的是延遲隊列,弊端同newCachedThreadPool一致。
那么上面說了使用Executors工具類創(chuàng)建的線程池有隱患,那如何使用才能避免這個隱患呢?如何才是最優(yōu)雅的方式去使用過線程池嗎?
生產環(huán)境要怎么去配置自己的線程池才是合理的呢?需要對癥下藥,建立自己的線程工廠類,靈活設置關鍵參數(shù)。
2、ThreadPoolExecutor 類
要自定義線程池,需要使用ThreadPoolExecutor類。
ThreadPoolExecutor類的構造方法:
public ThreadPoolExecutor(int coreSize,int maxSize,long KeepAliveTime,TimeUnit unit,BlockingQueue queue,ThreadFactory factory,RejectedExectionHandler handler)
上述構造方法共有七個參數(shù),這七個參數(shù)的含義分別是:
corePoolSize: 核心線程數(shù),也是線程池中常駐的線程數(shù),線程池初始化時默認是沒有線程的,當任務來臨時才開始創(chuàng)建線程去執(zhí)行任務
maximumPoolSize: 最大線程數(shù),在核心線程數(shù)的基礎上可能會額外增加一些非核心線程,需要注意的是只有當
workQueue隊列填滿時才會創(chuàng)建多于corePoolSize的線程(線程池總線程數(shù)不超過maxPoolSize)keepAliveTime: 非核心線程的空閑時間超過
keepAliveTime就會被自動終止回收掉,注意當corePoolSize=maxPoolSize時,keepAliveTime參數(shù)也就不起作用了(因為不存在非核心線程);unit:
keepAliveTime的時間單位workQueue: 用于保存任務的隊列,可以為無界、有界、同步移交三種隊列類型之一,當池子里的工作線程數(shù)大于
corePoolSize時,這時新進來的任務會被放到隊列中threadFactory: 創(chuàng)建線程的工廠類,默認使用
Executors.defaultThreadFactory(),也可以使用guava庫的ThreadFactoryBuilder來創(chuàng)建handler: 線程池無法繼續(xù)接收任務(隊列已滿且線程數(shù)達到
maximunPoolSize)時的飽和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
3、線程池配置相關
3.1 線程池大小的設置
首先針對于這個問題,我們必須要明確我們的需求是計算密集型還是IO密集型,只有了解了這一點,我們才能更好的去設置線程池的數(shù)量進行限制。
計算密集型
顧名思義就是應用需要非常多的CPU計算資源,在多核CPU時代,我們要讓每一個CPU核心都參與計算,將CPU的性能充分利用起來,這樣才算是沒有浪費服務器配置,如果在非常好的服務器配置上還運行著單線程程序那將是多么重大的浪費。對于計算密集型的應用,完全是靠CPU的核數(shù)來工作,所以為了讓它的優(yōu)勢完全發(fā)揮出來,避免過多的線程上下文切換,比較理想方案是:
線程數(shù) = CPU核數(shù)+1,也可以設置成CPU核數(shù)*2,但還要看JDK的版本以及CPU配置(服務器的CPU有超線程)。
一般設置CPU * 2即可。
IO密集型
我們現(xiàn)在做的開發(fā)大部分都是WEB應用,涉及到大量的網絡傳輸,不僅如此,與數(shù)據(jù)庫,與緩存間的交互也涉及到IO,一旦發(fā)生IO,線程就會處于等待狀態(tài),當IO結束,數(shù)據(jù)準備好后,線程才會繼續(xù)執(zhí)行。
因此從這里可以發(fā)現(xiàn),對于IO密集型的應用,我們可以多設置一些線程池中線程的數(shù)量,這樣就能讓在等待IO的這段時間內,線程可以去做其它事,提高并發(fā)處理效率。那么這個線程池的數(shù)據(jù)量是不是可以隨便設置呢?當然不是的,請一定要記得,線程上下文切換是有代價的。目前總結了一套公式,對于IO密集型應用:
線程數(shù) = CPU核心數(shù)/(1-阻塞系數(shù)) 這個阻塞系數(shù)一般為0.8~0.9之間,也可以取0.8或者0.9。
套用公式,對于雙核CPU來說,它比較理想的線程數(shù)就是20,當然這都不是絕對的,需要根據(jù)實際情況以及實際業(yè)務來調整:final int poolSize = (int)(cpuCore/(1-0.9))
針對于阻塞系數(shù),《Programming Concurrency on the JVM Mastering》即《Java 虛擬機并發(fā)編程》中有提到一句話:
對于阻塞系數(shù),我們可以先試著猜測,抑或采用一些細嫩分析工具或
java.lang.managementAPI 來確定線程花在系統(tǒng)/IO操作上的時間與CPU密集任務所耗的時間比值。
3.2 線程池相關參數(shù)配置
一定不要選擇沒有上限限制的配置項。
這也是為什么不建議使用 Executors 中創(chuàng)建線程的方法。
例如,Executors.newCachedThreadPool 的設置與無界隊列的設置因為某些不可預期的情況,線程池會出現(xiàn)系統(tǒng)異常,導致線程暴增的情況或者任務隊列不斷膨脹,內存耗盡導致系統(tǒng)崩潰和異常。
推薦使用自定義線程池來避免該問題,這也是在使用線程池規(guī)范的首要原則!
第二,合理設置線程數(shù)量、和線程空閑回收時間,
根據(jù)具體的任務執(zhí)行周期和時間去設定,避免頻繁的回收和創(chuàng)建,雖然我們使用線程池的目的是為了提升系統(tǒng)性能和吞吐量,但是也要考慮下系統(tǒng)的穩(wěn)定性,不然出現(xiàn)不可預期問題會很麻煩!
第三,根據(jù)實際場景,選擇適用于自己的拒絕策略。
進行補償,不要亂用JDK支持的自動補償機制!盡量采用自定義的拒絕策略去進行兜底!
第四,線程池拒絕策略,自定義拒絕策略可以實現(xiàn)RejectedExecutionHandler接口。
JDK自帶的拒絕策略如下:
AbortPolicy:直接拋出異常阻止系統(tǒng)正常工作。CallerRunsPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。DiscardOldestPolicy:丟棄最老的一個請求,嘗試再次提交當前任務。DiscardPolicy:丟棄無法處理的任務,不給予任何處理。
4、利用Hook
利用Hook,留下線程池執(zhí)行軌跡:
ThreadPoolExecutor提供了protected類型可以被覆蓋的鉤子方法,允許用戶在任務執(zhí)行之前會執(zhí)行之后做一些事情。我們可以通過它來實現(xiàn)比如初始化ThreadLocal、收集統(tǒng)計信息、如記錄日志等操作。這類Hook如beforeExecute和afterExecute。另外還有一個Hook可以用來在任務被執(zhí)行完的時候讓用戶插入邏輯,如rerminated 。
如果hook方法執(zhí)行失敗,則內部的工作線程的執(zhí)行將會失敗或被中斷。
我們可以使用beforeExecute和afterExecute來記錄線程之前前和后的一些運行情況,也可以直接把運行完成后的狀態(tài)記錄到ELK等日志系統(tǒng)。
5、關閉線程池
當線程池不再被引用并且工作線程數(shù)為0的時候,線程池將被終止。我們也可以調用shutdown來手動終止線程池。如果我們忘記調用shutdown,為了讓線程資源被釋放,我們還可以使用keepAliveTime 和 allowCoreThreadTimeOut來達到目的!
當然,穩(wěn)妥的方式是使用虛擬機Runtime.getRuntime().addShutdownHook方法,手工去調用線程池的關閉方法。
6、可優(yōu)化事項
6.1 設置線程池中線程為Daemon
一般情況下,關閉線程池后,線程池會自行將其中的線程結束掉。但針對一些自己偽裝或直接new Thread()的這種線程,則仍會阻塞進程關閉。
按照,java進程關閉判定方法,當只存在Daemon線程時,進程才會正常關閉。因此,這里建議這些非主要線程均設置為 daemon,即不會阻塞進程關閉。
6.2 正確命名Thread
在使用線程池時,一般會接受 ThreadFactory 對象,來控制如何創(chuàng)建thread。在java自帶的ExecutorService時,如果沒有設置此參數(shù),則會使用默認的 DefaultThreadFactory。效果就是,你會在線程棧列表中,看到一堆的 pool-x-thread-y,在實際使用 jstack時,根本看不清這些線程每個所屬的組,以及具體作用。
6.3 丟棄不再可用周期性任務
一般情況下,使用 java 自帶的 ScheduledThreadPoolExecutor, 調用 scheduleAtFixedRate 及 scheduleWithFixedDelay 均會將任務設置為周期性的(period)。在線程池關閉時,這些任務均可以直接被丟棄掉(默認情況下). 但如果使用 schedule 添加遠期的任務時,線程池則會因為其不是周期性任務而不會關閉所對應的線程
如 spring 體系中 TriggerTask(包括CronTask), 來進行定時調度的任務,其最終均是通過 schedule 來實現(xiàn)調度,并在單個任務完成之后,再次 schedule 下一次任務的方式來執(zhí)行。這種方式會被認為并不是 period. 因此,使用此調度方式時,盡管容器關閉時,執(zhí)行了 shutdown 方法,但相應底層的 ScheduledExecutorService 仍然不會成功關閉掉(盡管所有的狀態(tài)均已經設置完)。最終效果就是,會看到一個已經處于shutdown狀態(tài)的線程池,但線程仍然在運行(狀態(tài)為 wait 任務)的情況.
為解決此方法,java 提供一個額外的設置參數(shù) executeExistingDelayedTasksAfterShutdown, 此值默認為true,即 shutdown 之后,仍然執(zhí)行。可以通過在定義線程池時將其設置為 false,即線程池關閉之后,不再運行這些延時任務。
來源:blog.csdn.net/ztchun/article/details/116602405
最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結構等等。
獲取方式:點“在看”,關注公眾號并回復 Java 領取,更多內容陸續(xù)奉上。
PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內容,記得讀完點一下“在看”,加個“星標”,這樣每次新文章推送才會第一時間出現(xiàn)在你的訂閱列表里。
點“在看”支持小哈呀,謝謝啦??

