java線程池的拒絕策略

一、爲什麼要自定義線程池

阿里規範中對於線程、線程池的規定

《阿里巴巴 Java開發手冊》1.6併發處理

第3條規定:線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程

第4條規定:線程池不允許使用Executors創建,而是通過ThreadPoolExecutor的方式創建,這樣的處理方式能讓編寫代碼的攻城獅更加明確線程池的運行規則,規避資源耗盡(OOM)的風險

之所以會出現這樣的規範,是因爲jdk已經封裝好的線程池存在潛在風險:

  • FixedThreadPool 和 SingleThreadPool:
    允許的請求隊列長度爲 Integer.MAX_VALUE ,會堆積大量請求OOM

  • CachedThreadPool 和 ScheduledThreadPool:
    允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量線程OOM

所以從系統安全角度出發,原則上都應該自己手動創建線程池

二、如何自定義線程池

ThreadPoolExecutor 有多個重載的構造函數。這裏使用參數最多的一個簡要說明自定義線程池的關鍵參數。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

其實自定義線程池很簡便,就這麼幾個規則

  • 線程池的線程數量長期維持在 corePoolSize 個(核心線程數量)
  • 線程池的線程數量最大可以擴展到 maximumPoolSize 個
  • 在 corePoolSize ~ maximumPoolSize 這個區間的線程,一旦空閒超過keepAliveTime時間,就會被殺掉(時間單位)
  • 送來工作的線程數量超過最大數以後,送到 workQueue 裏面待業
  • 待業隊伍也滿了,就按照事先約定的策略 RejectedExecutionHandler 給拒絕掉

以下詳細解析拒絕策略

三、線程池的拒絕策略

3-0、所有拒絕策略都實現了接口 RejectedExecutionHandler

public interface RejectedExecutionHandler {

    /**
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

這個接口只有一個 rejectedExecution 方法。

r 爲待執行任務;executor 爲線程池;方法可能會拋出拒絕異常。

3-1、AbortPolicy

直接拋出拒絕異常(繼承自RuntimeException),會中斷調用者的處理過程,所以除非有明確需求,一般不推薦

    public static class AbortPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

3-2、CallerRunsPolicy

在調用者線程中(也就是說誰把 r 這個任務甩來的),運行當前被丟棄的任務。

只會用調用者所在線程來運行任務,也就是說任務不會進入線程池。

如果線程池已經被關閉,則直接丟棄該任務。

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

這裏有個小問題:r.run() 是如何做到使用調用者所在線程來運行任務的?

參看:Thread的.start()與.run()的區別

3-3、DiscardOledestPolicy

丟棄隊列中最老的,然後再次嘗試提交新任務。

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

這裏 e.getQueue() 是獲得待執行的任務隊列,也就是前面提到的待業隊列。

因爲是隊列,所以先進先出,一個poll()方法就能直接把隊列中最老的拋棄掉,再次嘗試執行execute®。

這個隊列在線程池定義的時候就能看到,是一個阻塞隊列

    /**
     * The queue used for holding tasks and handing off to worker
     * threads.  We do not require that workQueue.
     */     
    private final BlockingQueue<Runnable> workQueue;

    public BlockingQueue<Runnable> getQueue() {
        return workQueue;
    }

3-4、DiscardPolicy

默默丟棄無法加載的任務。

這個代碼就很簡單了,真的是啥也沒做

    public static class DiscardPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

3-5、通過實現 RejectedExecutionHandler 接口擴展

jdk內置的四種拒絕策略(都在ThreadPoolExecutor.java裏面)代碼都很簡潔易懂。

我們只要繼承接口都可以根據自己需要自定義拒絕策略。下面看兩個例子。

一是netty自己實現的線程池裏面私有的一個拒絕策略。單獨啓動一個新的臨時線程來執行任務。

    private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        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);
            }
        }
    }

另外一個是dubbo的一個例子,它直接繼承的 AbortPolicy ,加強了日誌輸出,並且輸出dump文件

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

    @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);
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章