前言
談到 JAVA 的線程池最熟悉的莫過于 ExecutorService 接口了,jdk1.5 新增的 java.util.concurrent 包下的這個 api,大大的簡化了多線程代碼的開發(fā)。而不論你用 FixedThreadPool 還是 CachedThreadPool 其背后實現(xiàn)都是ThreadPoolExecutor。ThreadPoolExecutor 是一個典型的緩存池化設(shè)計的產(chǎn)物,因為池子有大小,當(dāng)池子體積不夠承載時,就涉及到拒絕策略。JDK 中已經(jīng)預(yù)設(shè)了 4 種線程池拒絕策略,下面結(jié)合場景詳細(xì)聊聊這些策略的使用場景,以及我們還能擴(kuò)展哪些拒絕策略。
池化設(shè)計思想
池話設(shè)計應(yīng)該不是一個新名詞。我們常見的如 Java 線程池、JDBC 連接池、redis 連接池等就是這類設(shè)計的代表實現(xiàn)。這種設(shè)計會初始預(yù)設(shè)資源,解決的問題就是抵消每次獲取資源的消耗,如創(chuàng)建線程的開銷,獲取遠(yuǎn)程連接的開銷等。就好比你去食堂打飯,打飯的大媽會先把飯盛好幾份放那里,你來了就直接拿著飯盒加菜即可,不用再臨時又盛飯又打菜,效率就高了。除了初始化資源,池化設(shè)計還包括如下這些特征:池子的初始值、池子的活躍值、池子的最大值等,這些特征可以直接映射到 Java 線程池和數(shù)據(jù)庫連接池的成員屬性中。
線程池觸發(fā)拒絕策略的時機(jī)
和數(shù)據(jù)源連接池不一樣,線程池除了初始大小和池子最大值,還多了一個阻塞隊列來緩沖。數(shù)據(jù)源連接池一般請求的連接數(shù)超過連接池的最大值的時候就會觸發(fā)拒絕策略,策略一般是阻塞等待設(shè)置的時間或者直接拋異常。而線程池的觸發(fā)時機(jī)如下圖:
如圖,想要了解線程池什么時候觸發(fā)拒絕粗略,需要明確上面三個參數(shù)的具體含義,是這三個參數(shù)總體協(xié)調(diào)的結(jié)果,而不是簡單的超過最大線程數(shù)就會觸發(fā)線程拒絕粗略,當(dāng)提交的任務(wù)數(shù)大于 corePoolSize 時,會優(yōu)先放到隊列緩沖區(qū),只有填滿了緩沖區(qū)后,才會判斷當(dāng)前運行的任務(wù)是否大于 maxPoolSize,小于時會新建線程處理。大于時就觸發(fā)了拒絕策略,總結(jié)就是:當(dāng)前提交任務(wù)數(shù)大于(maxPoolSize + queueCapacity)時就會觸發(fā)線程池的拒絕策略了。
JDK內(nèi)置4種線程池拒絕策略
拒絕策略接口定義
在分析 JDK 自帶的線程池拒絕策略前,先看下 JDK 定義的 拒絕策略接口,如下:
1public interface RejectedExecutionHandler {
2 void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
3}
接口定義很明確,當(dāng)觸發(fā)拒絕策略時,線程池會調(diào)用你設(shè)置的具體的策略,將當(dāng)前提交的任務(wù)以及線程池實例本身傳遞給你處理,具體作何處理,不同場景會有不同的考慮,下面看 JDK 為我們內(nèi)置了哪些實現(xiàn):
CallerRunsPolicy(調(diào)用者運行策略)
1 public static class CallerRunsPolicy implements RejectedExecutionHandler {
2
3 public CallerRunsPolicy { }
4
5 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6 if (!e.isShutdown) {
7 r.run;
8 }
9 }
10 }
功能:當(dāng)觸發(fā)拒絕策略時,只要線程池沒有關(guān)閉,就由提交任務(wù)的當(dāng)前線程處理。
使用場景:一般在不允許失敗的、對性能要求不高、并發(fā)量較小的場景下使用,因為線程池一般情況下不會關(guān)閉,也就是提交的任務(wù)一定會被運行,但是由于是調(diào)用者線程自己執(zhí)行的,當(dāng)多次提交任務(wù)時,就會阻塞后續(xù)任務(wù)執(zhí)行,性能和效率自然就慢了。
AbortPolicy(中止策略)
1 public static class AbortPolicy implements RejectedExecutionHandler {
2
3 public AbortPolicy { }
4
5 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6 throw new RejectedExecutionException("Task " + r.toString +
7 " rejected from " +
8 e.toString);
9 }
10 }
功能:當(dāng)觸發(fā)拒絕策略時,直接拋出拒絕執(zhí)行的異常,中止策略的意思也就是打斷當(dāng)前執(zhí)行流程
使用場景:這個就沒有特殊的場景了,但是一點要正確處理拋出的異常。ThreadPoolExecutor 中默認(rèn)的策略就是AbortPolicy,ExecutorService 接口的系列 ThreadPoolExecutor 因為都沒有顯示的設(shè)置拒絕策略,所以默認(rèn)的都是這個。但是請注意,ExecutorService 中的線程池實例隊列都是無界的,也就是說把內(nèi)存撐爆了都不會觸發(fā)拒絕策略。當(dāng)自己自定義線程池實例時,使用這個策略一定要處理好觸發(fā)策略時拋的異常,因為他會打斷當(dāng)前的執(zhí)行流程。
DiscardPolicy(丟棄策略)
1 public static class DiscardPolicy implements RejectedExecutionHandler {
2
3 public DiscardPolicy { }
4
5 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6 }
7 }
功能:直接靜悄悄的丟棄這個任務(wù),不觸發(fā)任何動作
使用場景:如果你提交的任務(wù)無關(guān)緊要,你就可以使用它 。因為它就是個空實現(xiàn),會悄無聲息的吞噬你的的任務(wù)。所以這個策略基本上不用了
DiscardOldestPolicy(棄老策略)
1 public static class DiscardOldestPolicy implements RejectedExecutionHandler {
2
3 public DiscardOldestPolicy { }
4
5 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6 if (!e.isShutdown) {
7 e.getQueue.poll;
8 e.execute(r);
9 }
10 }
11 }
功能:如果線程池未關(guān)閉,就彈出隊列頭部的元素,然后嘗試執(zhí)行
使用場景:這個策略還是會丟棄任務(wù),丟棄時也是毫無聲息,但是特點是丟棄的是老的未執(zhí)行的任務(wù),而且是待執(zhí)行優(yōu)先級較高的任務(wù)。基于這個特性,我能想到的場景就是,發(fā)布消息,和修改消息,當(dāng)消息發(fā)布出去后,還未執(zhí)行,此時更新的消息又來了,這個時候未執(zhí)行的消息的版本比現(xiàn)在提交的消息版本要低就可以被丟棄了。因為隊列中還有可能存在消息版本更低的消息會排隊執(zhí)行,所以在真正處理消息的時候一定要做好消息的版本比較
第三方實現(xiàn)的拒絕策略
Dubbo 中的線程拒絕策略
1public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
2
3 protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
4
5 private final String threadName;
6
7 private final URL url;
8
9 private static volatile long lastPrintTime = 0;
10
11 private static Semaphore guard = new Semaphore(1);
12
13 public AbortPolicyWithReport(String threadName, URL url) {
14 this.threadName = threadName;
15 this.url = url;
16 }
17
18 @Override
19 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
20 String msg = String.format("Thread pool is EXHAUSTED!" +
21 " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
22 " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
23 threadName, e.getPoolSize, e.getActiveCount, e.getCorePoolSize, e.getMaximumPoolSize, e.getLargestPoolSize,
24 e.getTaskCount, e.getCompletedTaskCount, e.isShutdown, e.isTerminated, e.isTerminating,
25 url.getProtocol, url.getIp, url.getPort);
26 logger.warn(msg);
27 dumpJStack;
28 throw new RejectedExecutionException(msg);
29 }
30
31 private void dumpJStack {
32 //省略實現(xiàn)
33 }
34}
可以看到,當(dāng)dubbo的工作線程觸發(fā)了線程拒絕后,主要做了三個事情,原則就是盡量讓使用者清楚觸發(fā)線程拒絕策略的真實原因
-
輸出了一條警告級別的日志,日志內(nèi)容為線程池的詳細(xì)設(shè)置參數(shù),以及線程池當(dāng)前的狀態(tài),還有當(dāng)前拒絕任務(wù)的一些詳細(xì)信息。可以說,這條日志,使用dubbo的有過生產(chǎn)運維經(jīng)驗的或多或少是見過的,這個日志簡直就是日志打印的典范,其他的日志打印的典范還有spring。得益于這么詳細(xì)的日志,可以很容易定位到問題所在
-
輸出當(dāng)前線程堆棧詳情,這個太有用了,當(dāng)你通過上面的日志信息還不能定位問題時,案發(fā)現(xiàn)場的dump線程上下文信息就是你發(fā)現(xiàn)問題的救命稻草,這個可以參考《dubbo線程池耗盡事件-"CyclicBarrier惹的禍"》
-
繼續(xù)拋出拒絕執(zhí)行異常,使本次任務(wù)失敗,這個繼承了JDK默認(rèn)拒絕策略的特性
Netty 中的線程池拒絕策略
1 private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
2 NewThreadRunsPolicy {
3 super;
4 }
5
6 public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
7 try {
8 final Thread t = new Thread(r, "Temporary task executor");
9 t.start;
10 } catch (Throwable e) {
11 throw new RejectedExecutionException(
12 "Failed to start a new thread", e);
13 }
14 }
15 }
Netty 中的實現(xiàn)很像 JDK 中的 CallerRunsPolicy,舍不得丟棄任務(wù)。不同的是,CallerRunsPolicy 是直接在調(diào)用者線程執(zhí)行的任務(wù)。而 Netty是新建了一個線程來處理的。所以,Netty的實現(xiàn)相較于調(diào)用者執(zhí)行策略的使用面就可以擴(kuò)展到支持高效率高性能的場景了。但是也要注意一點,Netty的實現(xiàn)里,在創(chuàng)建線程時未做任何的判斷約束,也就是說只要系統(tǒng)還有資源就會創(chuàng)建新的線程來處理,直到new不出新的線程了,才會拋創(chuàng)建線程失敗的異常
ActiveMQ 中的線程池拒絕策略
1 new RejectedExecutionHandler {
2 @Override
3 public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
4 try {
5 executor.getQueue.offer(r, 60, TimeUnit.SECONDS);
6 } catch (InterruptedException e) {
7 throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
8 }
9
10 throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
11 }
12 });
activeMq中的策略屬于最大努力執(zhí)行任務(wù)型,當(dāng)觸發(fā)拒絕策略時,在嘗試一分鐘的時間重新將任務(wù)塞進(jìn)任務(wù)隊列,當(dāng)一分鐘超時還沒成功時,就拋出異常
PinPoint 中的線程池拒絕策略
1public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
2 private final RejectedExecutionHandler handlerChain;
3
4 public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
5 Objects.requireNon(chain, "handlerChain must not be ");
6 RejectedExecutionHandler handlerChain = chain.toArray(new RejectedExecutionHandler[0]);
7 return new RejectedExecutionHandlerChain(handlerChain);
8 }
9
10 private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
11 this.handlerChain = Objects.requireNon(handlerChain, "handlerChain must not be ");
12 }
13
14 @Override
15 public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
16 for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) {
17 rejectedExecutionHandler.rejectedExecution(r, executor);
18 }
19 }
20}
pinpoint的拒絕策略實現(xiàn)很有特點,和其他的實現(xiàn)都不同。他定義了一個拒絕策略鏈,包裝了一個拒絕策略列表,當(dāng)觸發(fā)拒絕策略時,會將策略鏈中的rejectedExecution依次執(zhí)行一遍
結(jié)語
前文從線程池設(shè)計思想,以及線程池觸發(fā)拒絕策略的時機(jī)引出java線程池拒絕策略接口的定義。并輔以JDK內(nèi)置4種以及四個第三方開源軟件的拒絕策略定義描述了線程池拒絕策略實現(xiàn)的各種思路和使用場景。希望閱讀此文后能讓你對java線程池拒絕策略有更加深刻的認(rèn)識,能夠根據(jù)不同的使用場景更加靈活的應(yīng)用。
作者:KL
鏈接:http://www.kailing.pub/article/index/arcid/255.html