[第二篇]深入學習線程池之通過ThreadPoolExecutor創建線程池及工作原理

一、創建線程池的方式

在實際的開發過程中,可以通過Executors類和ThreadPoolExecutor類來進行線程池的創建。但是,在《阿里巴巴java開發手冊》中,不建議使用Executors類來創建線程池,原因之一是通過Executors類創建的四大線程池,可能會發生OOM問題,之二是通過Executors大家可以先做一下了解,本文后面會闡述產生OOM的原因。

二、基于ThreadPoolExecutor類進行線程池的創建

線程池的真正實現類是ThreadPoolExecutor,其構造方法有如下4種:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

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

1. 線程池的參數

corePoolSize(必需):核心線程數。默認情況下,核心線程會一直存活,但是當將allowCoreThreadTimeout設置為true時,核心線程也會超時回收。

maximumPoolSize(必需):線程池所能容納的最大線程數。當活躍線程數達到該數值后,后續的新任務將會阻塞。

keepAliveTime(必需):線程閑置超時時長。如果超過該時長,非核心線程就會被回收。如果將allowCoreThreadTimeout設置為true時,核心線程也會超時回收。

unit(必需):指定keepAliveTime參數的時間單位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

workQueue(必需):任務隊列。通過線程池的execute()方法提交的Runnable對象將存儲在該參數中。

threadFactory(可選):線程工廠。用于指定為線程池創建新線程的方式。

handler(可選):拒絕策略。當達到最大線程數時需要執行的飽和策略。

2. 任務隊列(workQueue)

任務隊列是基于阻塞隊列實現的,即采用生產者消費者模式,在Java中需要實現BlockingQueue接口。但Java已經為我們提供了7種阻塞隊列的實現:

ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列(數組結構可配合指針實現一個環形隊列)。

LinkedBlockingQueue: 一個由鏈表結構組成的有界阻塞隊列,在未指明容量時,容量默認為Integer.MAX_VALUE。

PriorityBlockingQueue: 一個支持優先級排序的無界阻塞隊列,對元素沒有要求,可以實現Comparable接口也可以提供Comparator來對隊列中的元素進行比較。跟時間沒有任何關系,僅僅是按照優先級取任務。

DelayQueue:類似于PriorityBlockingQueue,是二叉堆實現的無界優先級阻塞隊列。要求元素都實現Delayed接口,通過執行時延從隊列中提取任務,時間沒到任務取不出來。

SynchronousQueue: 一個不存儲元素的阻塞隊列,消費者線程調用take()方法的時候就會發生阻塞,直到有一個生產者線程生產了一個元素,消費者線程就可以拿到這個元素并返回;生產者線程調用put()方法的時候也會發生阻塞,直到有一個消費者線程消費了一個元素,生產者才會返回。

LinkedBlockingDeque: 使用雙向隊列實現的有界雙端阻塞隊列。雙端意味著可以像普通隊列一樣FIFO(先進先出),也可以像棧一樣FILO(先進后出)。

LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的結合體,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行為一致,但是是無界的阻塞隊列。

注意有界隊列和無界隊列的區別:如果使用有界隊列,當隊列飽和時并超過最大線程數時就會執行拒絕策略;而如果使用無界隊列,因為任務隊列永遠都可以添加任務,所以設置maximumPoolSize沒有任何意義。

3. 線程工廠(threadFactory)

線程工廠指定創建線程的方式,需要實現ThreadFactory接口,并實現newThread(Runnable r)方法。該參數可以不用指定,Executors框架已經為我們實現了一個默認的線程工廠,如下圖:

/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

在實際的開發中可以是用Google提供的guava去創建ThreadFactory,相關信息如下:

maven依賴:
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

使用demo:
ThreadFactory nameFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

4. 拒絕策略(handler)

當線程池的線程數達到最大線程數時,需要執行拒絕策略。拒絕策略需要實現RejectedExecutionHandler接口,并實現rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不過Executors框架已經為我們實現了4種拒絕策略:

AbortPolicy(默認):丟棄任務并拋出RejectedExecutionException異常。

CallerRunsPolicy:由調用線程處理該任務。

DiscardPolicy:丟棄任務,但是不拋出異常。可以配合這種模式進行自定義的處理方式。

DiscardOldestPolicy:丟棄隊列最早的未處理任務,然后重新嘗試執行任務。

三、線程池的工作原理

1、如果當前運行的線程少于corePoolSize,則創建新線程來執行任務(注意:執行這一步驟需要獲取全局鎖)。

2、如果運行的線程等于或多于corePoolSize,則將任務加入BlockingQueue。

3、如果無法將任務加入BlockingQueue(隊列已滿),則在非corePool中創建新的線程來處理任務(注意:執行這一步驟需要獲取全局鎖)。

4、如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,并調用
RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步驟的總體設計思路,一是為了在執行execute()方法時,盡可能地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸),二是創建和回收線程比較消耗資源。
在ThreadPoolExecutor當前運行的線程數大于等于corePoolSize時,幾乎所有的execute()方法調用都是執行步驟2,而步驟2不需要獲取全局鎖。

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

推薦閱讀更多精彩內容