一、創建線程池的方式
在實際的開發過程中,可以通過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不需要獲取全局鎖。