Java執行緒池8大拒絕策略,面試必問!

JAVA一方發表於2020-02-19

前言

談到java的執行緒池最熟悉的莫過於ExecutorService介面了,jdk1.5新增的java.util.concurrent包下的這個api,大大的簡化了多執行緒程式碼的開發。而不論你用FixedThreadPool還是CachedThreadPool其背後實現都是ThreadPoolExecutor。

ThreadPoolExecutor是一個典型的快取池化設計的產物,因為池子有大小,當池子體積不夠承載時,就涉及到拒絕策略。JDK中已經預設了4種執行緒池拒絕策略,下面結合場景詳細聊聊這些策略的使用場景,以及我們還能擴充套件哪些拒絕策略。

池化設計思想

池話設計應該不是一個新名詞。我們常見的如java執行緒池、jdbc連線池、redis連線池等就是這類設計的代表實現。

這種設計會初始預設資源,解決的問題就是抵消每次獲取資源的消耗,如建立執行緒的開銷,獲取遠端連線的開銷等。就好比你去食堂打飯,打飯的大媽會先把飯盛好幾份放那裡,你來了就直接拿著飯盒加菜即可,不用再臨時又盛飯又打菜,效率就高了。

除了初始化資源,池化設計還包括如下這些特徵:池子的初始值、池子的活躍值、池子的最大值等,這些特徵可以直接對映到java執行緒池和資料庫連線池的成員屬性中。

執行緒池觸發拒絕策略的時機

和資料來源連線池不一樣,執行緒池除了初始大小和池子最大值,還多了一個阻塞佇列來緩衝。

資料來源連線池一般請求的連線數超過連線池的最大值的時候就會觸發拒絕策略,策略一般是阻塞等待設定的時間或者直接拋異常。

image
image

如圖,想要了解執行緒池什麼時候觸發拒絕粗略,需要明確上面三個引數的具體含義,是這三個引數總體協調的結果,而不是簡單的超過最大執行緒數就會觸發執行緒拒絕粗略,當提交的任務數大於corePoolSize時,會優先放到佇列緩衝區,只有填滿了緩衝區後,才會判斷當前執行的任務是否大於maxPoolSize,小於時會新建執行緒處理。大於時就觸發了拒絕策略。

總結就是:當前提交任務數大於(maxPoolSize + queueCapacity)時就會觸發執行緒池的拒絕策略了。

JDK內建4種執行緒池拒絕策略

拒絕策略介面定義

在分析JDK自帶的執行緒池拒絕策略前,先看下JDK定義的 拒絕策略介面,如下:

public interface RejectedExecutionHandler {

void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

}

介面定義很明確,當觸發拒絕策略時,執行緒池會呼叫你設定的具體的策略,將當前提交的任務以及執行緒池例項本身傳遞給你處理,具體作何處理,不同場景會有不同的考慮,下面看JDK為我們內建了哪些實現:

CallerRunsPolicy(呼叫者執行策略)

**public static class CallerRunsPolicy implements RejectedExecutionHandler { **

**public CallerRunsPolicy() { } **

** public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { **

** if (!e.isShutdown()) { **

** r.run();**

** }**

** } **

** }**

功能:當觸發拒絕策略時,只要執行緒池沒有關閉,就由提交任務的當前執行緒處理。

使用場景:一般在不允許失敗的、對效能要求不高、併發量較小的場景下使用,因為執行緒池一般情況下不會關閉,也就是提交的任務一定會被執行,但是由於是呼叫者執行緒自己執行的,當多次提交任務時,就會阻塞後續任務執行,效能和效率自然就慢了。

AbortPolicy(中止策略)

public static class AbortPolicy implements RejectedExecutionHandler {

  public AbortPolicy() { } 

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 

        throw new RejectedExecutionException("Task " + r.toString() + 

                                            " rejected from " + 

                                            e.toString()); 

    } 

}
複製程式碼

功能:當觸發拒絕策略時,直接丟擲拒絕執行的異常,中止策略的意思也就是打斷當前執行流程

使用場景:這個就沒有特殊的場景了,但是一點要正確處理丟擲的異常。

ThreadPoolExecutor中預設的策略就是AbortPolicy,ExecutorService介面的系列ThreadPoolExecutor因為都沒有顯示的設定拒絕策略,所以預設的都是這個。

但是請注意,ExecutorService中的執行緒池例項佇列都是無界的,也就是說把記憶體撐爆了都不會觸發拒絕策略。當自己自定義執行緒池例項時,使用這個策略一定要處理好觸發策略時拋的異常,因為他會打斷當前的執行流程。

DiscardPolicy(丟棄策略)

public static class DiscardPolicy implements RejectedExecutionHandler {

    public DiscardPolicy() { } 

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 

    } 

}
複製程式碼

功能:直接靜悄悄的丟棄這個任務,不觸發任何動作

使用場景:如果你提交的任務無關緊要,你就可以使用它 。因為它就是個空實現,會悄無聲息的吞噬你的的任務。所以這個策略基本上不用了

DiscardOldestPolicy(棄老策略)

**public static class DiscardOldestPolicy implements RejectedExecutionHandler { **

** public DiscardOldestPolicy() { } **

** public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { **

** if (!e.isShutdown()) { **

** e.getQueue().poll(); **

** e.execute(r); **

** } **

** } **

** }**

功能:如果執行緒池未關閉,就彈出佇列頭部的元素,然後嘗試執行

使用場景:這個策略還是會丟棄任務,丟棄時也是毫無聲息,但是特點是丟棄的是老的未執行的任務,而且是待執行優先順序較高的任務。

基於這個特性,我能想到的場景就是,釋出訊息,和修改訊息,當訊息釋出出去後,還未執行,此時更新的訊息又來了,這個時候未執行的訊息的版本比現在提交的訊息版本要低就可以被丟棄了。因為佇列中還有可能存在訊息版本更低的訊息會排隊執行,所以在真正處理訊息的時候一定要做好訊息的版本比較。

第三方實現的拒絕策略

dubbo中的執行緒拒絕策略

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);

private final String threadName;

private final URL url; 

private static volatile long lastPrintTime = 0; 

private static Semaphore guard = new Semaphore(1); 

public AbortPolicyWithReport(String threadName, URL url) { 
複製程式碼

this.threadName = threadName;

this.url = url;

} 

@Override 

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 

    String msg = String.format("Thread pool is EXHAUSTED!" + 

                    " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," + 

                    " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!", 

            threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(),
複製程式碼

e.getMaximumPoolSize(), e.getLargestPoolSize(),

            e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(), 

            url.getProtocol(), url.getIp(), url.getPort()); 

    logger.warn(msg); 

    dumpJStack(); 

    throw new RejectedExecutionException(msg); 

} 

private void dumpJStack() { 

  //省略實現 

} 
複製程式碼

}

可以看到,當dubbo的工作執行緒觸發了執行緒拒絕後,主要做了三個事情,原則就是儘量讓使用者清楚觸發執行緒拒絕策略的真實原因。

1)輸出了一條警告級別的日誌,日誌內容為執行緒池的詳細設定引數,以及執行緒池當前的狀態,還有當前拒絕任務的一些詳細資訊。可以說,這條日誌,使用dubbo的有過生產運維經驗的或多或少是見過的,這個日誌簡直就是日誌列印的典範,其他的日誌列印的典範還有spring。得益於這麼詳細的日誌,可以很容易定位到問題所在

2)輸出當前執行緒堆疊詳情,這個太有用了,當你通過上面的日誌資訊還不能定位問題時,案發現場的dump執行緒上下文資訊就是你發現問題的救命稻草。

3)繼續丟擲拒絕執行異常,使本次任務失敗,這個繼承了JDK預設拒絕策略的特性

擴充套件閱讀:Dubbo 面試18問,你能接得住嗎?

Netty中的執行緒池拒絕策略

private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {

    NewThreadRunsPolicy() { 

        super(); 

    } 

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 

        try { 

            final Thread t = new Thread(r, "Temporary task executor"); 

            t.start(); 

        } catch (Throwable e) { 

            throw new RejectedExecutionException( 

                    "Failed to start a new thread", e); 

        } 

    } 

}
複製程式碼

Netty中的實現很像JDK中的CallerRunsPolicy,捨不得丟棄任務。不同的是,CallerRunsPolicy是直接在呼叫者執行緒執行的任務。而 Netty是新建了一個執行緒來處理的。

所以,Netty的實現相較於呼叫者執行策略的使用面就可以擴充套件到支援高效率高效能的場景了。但是也要注意一點,Netty的實現裡,在建立執行緒時未做任何的判斷約束,也就是說只要系統還有資源就會建立新的執行緒來處理,直到new不出新的執行緒了,才會拋建立執行緒失敗的異常。推薦:什麼是Netty?

activeMq中的執行緒池拒絕策略

new RejectedExecutionHandler() {

            @Override 

            public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { 

                try { 

                    executor.getQueue().offer(r, 60, TimeUnit.SECONDS); 

                } catch (InterruptedException e) { 

                    throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); 

                } 

                throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); 

            } 

        });
複製程式碼

activeMq中的策略屬於最大努力執行任務型,當觸發拒絕策略時,在嘗試一分鐘的時間重新將任務塞進任務佇列,當一分鐘超時還沒成功時,就丟擲異常

pinpoint中的執行緒池拒絕策略

public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {

private final RejectedExecutionHandler[] handlerChain;

public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) { 

    Objects.requireNonNull(chain, "handlerChain must not be null"); 

    RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]); 

    return new RejectedExecutionHandlerChain(handlerChain); 

} 

private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) { 
複製程式碼

this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");

} 

@Override 

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 

    for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) { 

        rejectedExecutionHandler.rejectedExecution(r, executor); 

    } 

} 
複製程式碼

}

pinpoint的拒絕策略實現很有特點,和其他的實現都不同。他定義了一個拒絕策略鏈,包裝了一個拒絕策略列表,當觸發拒絕策略時,會將策略鏈中的rejectedExecution依次執行一遍。

更多關於Java面試資料領取途徑,點選:程式設計師

結語

前文從執行緒池設計思想,以及執行緒池觸發拒絕策略的時機引出java執行緒池拒絕策略介面的定義。並輔以JDK內建4種以及四個第三方開源軟體的拒絕策略定義描述了執行緒池拒絕策略實現的各種思路和使用場景。

希望閱讀此文後能讓你對java執行緒池拒絕策略有更加深刻的認識,能夠根據不同的使用場景更加靈活的應用


相關文章