自定義線程池

前言

最近在看并發編程藝術這本書,對看書的一些筆記及個人工作中的總結。

hashiqi.jpeg

線程池的優勢

Java中的線程池是運用場景最多的并發框架,幾乎所有需要異步或并發執行任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來3個好處。
第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用線程池,必須對其實現原理了如指掌。

線程池的實現原理

提交一個新的任務到線程池中,線程池的處理流程:
1)線程池判斷核心線程池里的線程是否都在執行任務。如果不是(也就是當前執行任務的線程數小于核心線程數corePoolSize),則創建一個新的線程執行任務。如果線程池中的線程都在執行任務,也就是說當前的工作線程數大于等于corePoolSize),則進入下個流程
2)線程池判斷工作隊列(一般是阻塞隊列BlockingQueue)是否已經滿了。如果工作隊列沒有滿,則將任務增加到工作隊列中。如果工作隊列滿了,則進入下個流程,
3)線程池判斷線程池的線程是否都處于工作狀態。如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略(或者叫做拒絕策略)來處理這個任務。

ThreadPoolExecutor執行execute方法分下面4種情況。
1)如果當前運行的線程少于corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
2)如果運行的線程等于或多于corePoolSize,則將任務加入BlockingQueue。
3)如果無法將任務加入BlockingQueue(隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。
4)如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,并調用RejectedExecutionHandler.rejectedExecution()方法。

下面是ThreadPoolExecutor的一個構造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}

稍微分析一下這幾個參數,官方api都有講:
corePoolSize:當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大于線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建并啟動所有基本線程。
maximumPoolSize:線程池中允許的最大線程數。如果隊列滿了,并且已創建的線程數小于最大線程數,則線程池會再創建新的線程執行任務。如果使用的是無界的隊列,那么這個參數沒有意義了。
keepAliveTime:線程池工作線程空閑后,保持的存活時間。
unit :上面時間的單位
workQueue:執行任務在執行之前加入的queue。該隊列僅保存由execute方法提交的Runnable任務。
threadFactory:創建新線程的工廠方法。可以通過線程工廠給每個創建的線程設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給線程池里的線程設置有意義的名字
handler:當隊列滿時,線程處于飽和策略。拒絕策略handler
jdk1.5提供了下面幾種策略:

圖片.png

AbortPolicy:直接拋異常。
DiscardPolicy:顧名思義,直接丟棄多余的任務。
DiscardOldestPolicy:丟棄隊列中最老的任務,并且重試exexute方法,并發書中說丟棄最新的一個任務,這邊書中應該描述錯了。
CallerRunsPolicy:在調用線程中執行丟棄的任務。

也可以自定義拒絕策略,比如記錄日志或者持久化處理。

執行源碼:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
}

執行線程次提交的任務

通過execute方法和submit方法(submit提供了重載方法)。execute不返回值,而submit返回值,并且可以通過返回的Future對象的get方法查看返回值。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

定義在ExecutorService類中:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
//BlockingQueue使用有界阻塞隊列
public class ThreadPoolExecutorTest1 {
    public static void main(String[] args) {
        /**
         * 在使用有界隊列時,若有新的任務需要執行,如果線程池實際線程數小于corePoolSize,則優先創建線程,
         * 若大于corePoolSize,則會將任務加入隊列,
         * 若隊列已滿,則在總線程數不大于maximumPoolSize的前提下,創建新的線程,
         * 若線程數大于maximumPoolSize,則執行拒絕策略。或其他自定義方式。
         *
         * 這個構造方法使用默認的拒絕策略,AbortPolicy(即拋出異常)
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                1,              //coreSize
                2,              //MaxSize
                60,             //60
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3)         //指定一種隊列 (有界隊列)
        );

        MyTask mt1 = new MyTask(1, "任務1");
        MyTask mt2 = new MyTask(2, "任務2");
        MyTask mt3 = new MyTask(3, "任務3");
        MyTask mt4 = new MyTask(4, "任務4");
        MyTask mt5 = new MyTask(5, "任務5");
        //MyTask mt6 = new MyTask(6, "任務6");

        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        //pool.execute(mt6);

        pool.shutdown();

    }
}
public class MyTask implements Runnable {

    private int taskId;
    private String taskName;

    public MyTask(int taskId, String taskName){
        this.taskId = taskId;
        this.taskName = taskName;
    }
    @Override
    public void run() {
        try {
            System.out.println("run taskId =" + this.taskId);
            Thread.sleep(5*1000);
            //System.out.println("end taskId =" + this.taskId);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String toString(){
        return Integer.toString(this.taskId);
    }

}

使用無界BlockingQueue:

與有界隊列相比,除非系統資源耗盡,否則無界的隊列不存在任務入隊失敗的情況。當有新任務到來,系統的線程小于corePoolSize時,則新建線程執行任務。當達到corePoolSize后,就不會繼續增加。若后續仍有新的隊列加入,而沒有空閑的線程資源,則隊列直接進入隊列等待。若任務創建和處理的速度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。
所以一般不建議使用無界隊列

//使用無界隊列的時候,最大線程數,拒絕策略等都失去了意義
public class ThreadPoolExecutorTest2 implements Runnable{
    private static AtomicInteger count = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            int temp = count.incrementAndGet();
            System.out.println("任務" + temp);
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        BlockingQueue<Runnable> queue =
                new LinkedBlockingQueue<>();
        ExecutorService executor  = new ThreadPoolExecutor(5, 10, 120L, TimeUnit.SECONDS, queue);

        for(int i = 0 ; i < 100; i++){
            executor.execute(new ThreadPoolExecutorTest2());
        }
        Thread.sleep(1000);
        System.out.println("queue size:" + queue.size());
        Thread.sleep(2000);
    }
}

自定義拒絕策略:

//和第一個demo相比,因為默認使用的策略是AbortPolicy(拋出一個拒絕異常),而我們這個使用了自定義的拒絕策略
public class ThreadPoolExecutorTest3 {

    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                1,              //coreSize
                2,              //MaxSize
                60,             //60
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3)         //指定一種隊列 (有界隊列)
                , new MyRejected()
//              , new DiscardOldestPolicy()
        );

        MyTask mt1 = new MyTask(1, "任務1");
        MyTask mt2 = new MyTask(2, "任務2");
        MyTask mt3 = new MyTask(3, "任務3");
        MyTask mt4 = new MyTask(4, "任務4");
        MyTask mt5 = new MyTask(5, "任務5");
        MyTask mt6 = new MyTask(6, "任務6");

        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        pool.execute(mt6);

        pool.shutdown();

    }
}
public class MyRejected implements RejectedExecutionHandler {
    public MyRejected(){
    }

    //拒絕策略實際調用的方法處理丟棄的任務,這邊可以持久化,可以進行日志處理
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("自定義處理..");
        System.out.println("當前被拒絕任務為:" + r.toString());
    }

}

下一篇博客將會分析一下jdk提供的Executor框架創建線程池的幾種底層實現原理。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容