讀完本文你將瞭解:
什麼是線程池
線程池的概念大家應該都很清楚,幫我們重複管理線程,避免創建大量的線程增加開銷。
除了降低開銷以外,線程池也可以提高響應速度,瞭解點 JVM 的同學可能知道,一個對象的創建大概需要經過以下幾步:
- 檢查對應的類是否已經被加載、解析和初始化
- 類加載後,爲新生對象分配內存
- 將分配到的內存空間初始爲 0
- 對對象進行關鍵信息的設置,比如對象的哈希碼等
- 然後執行 init 方法初始化對象
創建一個對象的開銷需要經過這麼多步,也是需要時間的嘛,那可以複用已經創建好的線程的線程池,自然也在提高響應速度上做了貢獻。
線程池的處理流程
創建線程池需要使用 ThreadPoolExecutor
類,它的構造函數參數如下:
public ThreadPoolExecutor(int corePoolSize, //核心線程的數量
int maximumPoolSize, //最大線程數量
long keepAliveTime, //超出核心線程數量以外的線程空餘存活時間
TimeUnit unit, //存活時間的單位
BlockingQueue<Runnable> workQueue, //保存待執行任務的隊列
ThreadFactory threadFactory, //創建新線程使用的工廠
RejectedExecutionHandler handler // 當任務無法執行時的處理器
) {...}
參數介紹如註釋所示,要了解這些參數左右着什麼,就需要了解線程池具體的執行方法ThreadPoolExecutor.execute
:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1.當前池中線程比核心數少,新建一個線程執行任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2.核心池已滿,但任務隊列未滿,添加到隊列中
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);
}
//3.核心池已滿,隊列已滿,試着創建一個新線程
else if (!addWorker(command, false))
reject(command); //如果創建新線程失敗了,說明線程池被關閉或者線程池完全滿了,拒絕任務
}
可以看到,線程池處理一個任務主要分三步處理,代碼註釋裏已經介紹了,我再用通俗易懂的例子解釋一下:
(線程比作員工,線程池比作一個團隊,核心池比作團隊中核心團隊員工數,核心池外的比作外包員工)
- 有了新需求,先看核心員工數量超沒超出最大核心員工數,還有名額的話就新招一個核心員工來做
- 需要獲取全局鎖
- 核心員工已經最多了,HR 不給批 HC 了,那這個需求只好攢着,放到待完成任務列表吧
- 如果列表已經堆滿了,核心員工基本沒機會搞完這麼多任務了,那就找個外包吧
- 需要獲取全局鎖
- 如果核心員工 + 外包員工的數量已經是團隊最多能承受人數了,沒辦法,這個需求接不了了
結合這張圖,這回流程你明白了嗎?
由於 1 和 3 新建線程時需要獲取全局鎖,這將嚴重影響性能。因此 ThreadPoolExecutor
這樣的處理流程是爲了在執行 execute()
方法時儘量少地執行 1 和 3,多執行 2。
在
ThreadPoolExecutor
完成預熱後(當前線程數不少於核心線程數),幾乎所有的execute()
都是在執行步驟 2。
前面提到的 ThreadPoolExecutor
構造函數的參數,分別影響以下內容:
corePoolSize
:核心線程池數量- 在線程數少於核心數量時,有新任務進來就新建一個線程,即使有的線程沒事幹
- 等超出核心數量後,就不會新建線程了,空閒的線程就得去任務隊列裏取任務執行了
maximumPoolSize
:最大線程數量- 包括核心線程池數量 + 核心以外的數量
- 如果任務隊列滿了,並且池中線程數小於最大線程數,會再創建新的線程執行任務
keepAliveTime
:核心池以外的線程存活時間,即沒有任務的外包的存活時間- 如果給線程池設置
allowCoreThreadTimeOut(true)
,則核心線程在空閒時頭上也會響起死亡的倒計時 - 如果任務是多而容易執行的,可以調大這個參數,那樣線程就可以在存活的時間裏有更大可能接受新任務
- 如果給線程池設置
workQueue
:保存待執行任務的阻塞隊列- 不同的任務類型有不同的選擇,下一小節介紹
threadFactory
:每個線程創建的地方- 可以給線程起個好聽的名字,設置個優先級啥的
handler
:飽和策略,大家都很忙,咋辦呢,有四種策略CallerRunsPolicy
:只要線程池沒關閉,就直接用調用者所在線程來運行任務AbortPolicy
:直接拋出RejectedExecutionException
異常DiscardPolicy
:悄悄把任務放生,不做了DiscardOldestPolicy
:把隊列裏待最久的那個任務扔了,然後再調用execute()
試試看能行不- 我們也可以實現自己的
RejectedExecutionHandler
接口自定義策略,比如如記錄日誌什麼的
保存待執行任務的阻塞隊列
當線程池中的核心線程數已滿時,任務就要保存到隊列中了。
線程池中使用的隊列是 BlockingQueue
接口,常用的實現有如下幾種:
- ArrayBlockingQueue:基於數組、有界,按 FIFO(先進先出)原則對元素進行排序
- LinkedBlockingQueue:基於鏈表,按FIFO (先進先出) 排序元素
- 吞吐量通常要高於 ArrayBlockingQueue
- Executors.newFixedThreadPool() 使用了這個隊列
- SynchronousQueue:不存儲元素的阻塞隊列
- 每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態
- 吞吐量通常要高於 LinkedBlockingQueue
- Executors.newCachedThreadPool使用了這個隊列
- PriorityBlockingQueue:具有優先級的、無限阻塞隊列
關於阻塞隊列的詳細介紹請看這篇:
創建自己的線程池
瞭解上面的內容後,我們就可以創建自己的線程池了。
①先定義線程池的幾個關鍵屬性的值:
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心線程數爲 CPU 數*2
private static final int MAXIMUM_POOL_SIZE = 64; // 線程池最大線程數
private static final int KEEP_ALIVE_TIME = 1; // 保持存活時間 1秒
- 設置核心池的數量爲 CPU 數的兩倍,一般是 4、8,好點的 16 個線程
- 最大線程數設置爲 64
- 空閒線程的存活時間設置爲 1 秒
②然後根據處理的任務類型選擇不同的阻塞隊列
如果是要求高吞吐量的,可以使用 SynchronousQueue
隊列;如果對執行順序有要求,可以使用 PriorityBlockingQueue
;如果最大積攢的待做任務有上限,可以使用 LinkedBlockingQueue
。
private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);
③然後創建自己的 ThreadFactory
在其中爲每個線程設置個名稱:
private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
};
④然後就可以創建線程池了
private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
new ThreadPoolExecutor.DiscardOldestPolicy());
- 這裏我們選擇的飽和策略爲
DiscardOldestPolicy
,你可以可以創建自己的。
⑤完整代碼:
public class ThreadPoolManager {
private final String TAG = this.getClass().getSimpleName();
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心線程數爲 CPU數*2
private static final int MAXIMUM_POOL_SIZE = 64; // 線程隊列最大線程數
private static final int KEEP_ALIVE_TIME = 1; // 保持存活時間 1秒
private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);
private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
};
private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
new ThreadPoolExecutor.DiscardOldestPolicy());
private static volatile ThreadPoolManager mInstance = new ThreadPoolManager();
public static ThreadPoolManager getInstance() {
return mInstance;
}
public void addTask(Runnable runnable) {
mExecutor.execute(runnable);
}
@Deprecated
public void shutdownNow() {
mExecutor.shutdownNow();
}
}
這樣我們就有了自己的線程池。
JDK 提供的線程池及使用場景
JDK 爲我們內置了五種常見線程池的實現,均可以使用 Executors
工廠類創建。
1.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
不招外包,有固定數量核心成員的正常互聯網團隊。
可以看到,FixedThreadPool
的核心線程數和最大線程數都是指定值,也就是說當線程池中的線程數超過核心線程數後,任務都會被放到阻塞隊列中。
此外 keepAliveTime
爲 0,也就是多餘的空餘線程會被立即終止(由於這裏沒有多餘線程,這個參數也沒什麼意義了)。
而這裏選用的阻塞隊列是 LinkedBlockingQueue
,使用的是默認容量 Integer.MAX_VALUE
,相當於沒有上限。
因此這個線程池執行任務的流程如下:
- 線程數少於核心線程數,也就是設置的線程數時,新建線程執行任務
- 線程數等於核心線程數後,將任務加入阻塞隊列
- 由於隊列容量非常大,可以一直加加加
- 執行完任務的線程反覆去隊列中取任務執行
FixedThreadPool
用於負載比較重的服務器,爲了資源的合理利用,需要限制當前線程數量。
2.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
不招外包,只有一個核心成員的創業團隊。
從參數可以看出來,SingleThreadExecutor
相當於特殊的 FixedThreadPool
,它的執行流程如下:
- 線程池中沒有線程時,新建一個線程執行任務
- 有一個線程以後,將任務加入阻塞隊列,不停加加加
- 唯一的這一個線程不停地去隊列裏取任務執行
聽起來很可憐的樣子 - -。
SingleThreadExecutor
用於串行執行任務的場景,每個任務必須按順序執行,不需要併發執行。
3.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
全部外包,沒活最多待 60 秒的外包團隊。
可以看到,CachedThreadPool
沒有核心線程,非核心線程數無上限,也就是全部使用外包,但是每個外包空閒的時間只有 60 秒,超過後就會被回收。
CachedThreadPool
使用的隊列是 SynchronousQueue
,這個隊列的作用就是傳遞任務,並不會保存。
因此當提交任務的速度大於處理任務的速度時,每次提交一個任務,就會創建一個線程。極端情況下會創建過多的線程,耗盡 CPU 和內存資源。
它的執行流程如下:
- 沒有核心線程,直接向
SynchronousQueue
中提交任務 - 如果有空閒線程,就去取出任務執行;如果沒有空閒線程,就新建一個
- 執行完任務的線程有 60 秒生存時間,如果在這個時間內可以接到新任務,就可以繼續活下去,否則就拜拜
由於空閒 60 秒的線程會被終止,長時間保持空閒的 CachedThreadPool
不會佔用任何資源。
CachedThreadPool
用於併發執行大量短期的小任務,或者是負載較輕的服務器。
4.newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
- 定期維護的 2B 業務團隊,核心與外包成員都有。
ScheduledThreadPoolExecutor
繼承自 ThreadPoolExecutor
, 最多線程數爲 Integer.MAX_VALUE
,使用 DelayedWorkQueue
作爲任務隊列。
ScheduledThreadPoolExecutor
添加任務和執行任務的機制與ThreadPoolExecutor
有所不同。
ScheduledThreadPoolExecutor
添加任務提供了另外兩個方法:
scheduleAtFixedRate()
:按某種速率週期執行scheduleWithFixedDelay()
:在某個延遲後執行
它倆的代碼如下:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0L)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0L)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
-unit.toNanos(delay),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
可以看到,這兩種方法都是創建了一個 ScheduledFutureTask
對象,調用 decorateTask()
方法轉成 RunnableScheduledFuture
對象,然後添加到隊列中。
看下 ScheduledFutureTask
的主要屬性:
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
//添加到隊列中的順序
private final long sequenceNumber;
//何時執行這個任務
private volatile long time;
//執行的間隔週期
private final long period;
//實際被添加到隊列中的 task
RunnableScheduledFuture<V> outerTask = this;
//在 delay queue 中的索引,便於取消時快速查找
int heapIndex;
//...
}
DelayQueue
中封裝了一個優先級隊列,這個隊列會對隊列中的 ScheduledFutureTask
進行排序,兩個任務的執行 time 不同時,time 小的先執行;否則比較添加到隊列中的順序 sequenceNumber ,先提交的先執行。
ScheduledThreadPoolExecutor
的執行流程如下:
- 調用上面兩個方法添加一個任務
- 線程池中的線程從 DelayQueue 中取任務
- 然後執行任務
具體執行任務的步驟也比較複雜:
- 線程從 DelayQueue 中獲取 time 大於等於當前時間的 ScheduledFutureTask
DelayQueue.take()
- 執行完後修改這個 task 的 time 爲下次被執行的時間
- 然後再把這個 task 放回隊列中
DelayQueue.add()
ScheduledThreadPoolExecutor
用於需要多個後臺線程執行週期任務,同時需要限制線程數量的場景。
兩種提交任務的方法
ExecutorService
提供了兩種提交任務的方法:
execute()
:提交不需要返回值的任務submit()
:提交需要返回值的任務
execute
void execute(Runnable command);
execute()
的參數是一個 Runnable,也沒有返回值。因此提交後無法判斷該任務是否被線程池執行成功。
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
@Override
public void run() {
//do something
}
});
submit
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
submit()
有三種重載,參數可以是 Callable
也可以是 Runnable
。
同時它會返回一個 Funture
對象,通過它我們可以判斷任務是否執行成功。
獲得執行結果調用 Future.get()
方法,這個方法會阻塞當前線程直到任務完成。
提交一個 Callable
任務時,需要使用 FutureTask
包一層:
FutureTask futureTask = new FutureTask(new Callable<String>() { //創建 Callable 任務
@Override
public String call() throws Exception {
String result = "";
//do something
return result;
}
});
Future<?> submit = executor.submit(futureTask); //提交到線程池
try {
Object result = submit.get(); //獲取結果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
關閉線程池
線程池即使不執行任務也會佔用一些資源,所以在我們要退出任務時最好關閉線程池。
有兩個方法關閉線程池:
`shutdown()
- 將線程池的狀態設置爲 SHUTDOWN,然後中斷所有沒有正在執行的線程
shutdownNow()
- 將線程池設置爲 STOP,然後嘗試停止所有線程,並返回等待執行任務的列表
它們的共同點是:都是通過遍歷線程池中的工作線程,逐個調用 Thread.interrup()
來中斷線程,所以一些無法響應中斷的任務可能永遠無法停止(比如 Runnable
)。
如何合理地選擇或者配置
瞭解 JDK 提供的幾種線程池實現,在實際開發中如何選擇呢?
根據任務類型決定。
前面已經介紹了,這裏再小節一下:
CachedThreadPool
用於併發執行大量短期的小任務,或者是負載較輕的服務器。FixedThreadPool
用於負載比較重的服務器,爲了資源的合理利用,需要限制當前線程數量。SingleThreadExecutor
用於串行執行任務的場景,每個任務必須按順序執行,不需要併發執行。ScheduledThreadPoolExecutor
用於需要多個後臺線程執行週期任務,同時需要限制線程數量的場景。
自定義線程池時,如果任務是 CPU 密集型(需要進行大量計算、處理),則應該配置儘量少的線程,比如 CPU 個數 + 1,這樣可以避免出現每個線程都需要使用很長時間但是有太多線程爭搶資源的情況;
如果任務是 IO密集型(主要時間都在 I/O,CPU 空閒時間比較多),則應該配置多一些線程,比如 CPU 數的兩倍,這樣可以更高地壓榨 CPU。
爲了錯誤避免創建過多線程導致系統奔潰,建議使用有界隊列。因爲它在無法添加更多任務時會拒絕任務,這樣可以提前預警,避免影響整個系統。
執行時間、順序有要求的話可以選擇優先級隊列,同時也要保證低優先級的任務有機會被執行。
總結
這篇文章簡單介紹了 Java 中線程池的工作原理和一些常見線程池的使用,在實際開發中最好使用線程池來統一管理異步任務,而不是直接 new 一個線程執行任務。
深入理解線程池及其原理
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:
如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。
那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?
在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池,首先我們從最核心的ThreadPoolExecutor類中的方法講起,然後再講述它的實現原理,接着給出了它的使用示例,最後討論了一下如何合理配置線程池的大小。
以下是本文的目錄大綱:
- 一.Java中的ThreadPoolExecutor類
- 二.深入剖析線程池實現原理
- 三.使用示例
- 四.如何合理配置線程池的大小
若有不正之處請多多諒解,並歡迎批評指正。
一.Java中的ThreadPoolExecutor類
java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類,因此如果要透徹地瞭解Java中的線程池,必須先了解這個類。下面我們來看一下ThreadPoolExecutor類的具體實現源碼。
在ThreadPoolExecutor類中提供了四個構造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
從上面的代碼可以得知,ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作。
下面解釋下一下構造器中各個參數的含義:
- corePoolSize:核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
- maximumPoolSize:線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程;
- keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0;
- unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
-
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小時 TimeUnit.MINUTES; //分鐘 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //納秒
- workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇:
-
ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
- threadFactory:線程工廠,主要用來創建線程;
- handler:表示當拒絕處理任務時的策略,有以下四種取值:
-
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程) ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
具體參數的配置與線程池的關係將在下一節講述。
從上面給出的ThreadPoolExecutor類的代碼可以知道,ThreadPoolExecutor繼承了AbstractExecutorService,我們來看一下AbstractExecutorService的實現:
public abstract class AbstractExecutorService implements ExecutorService {
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
public Future<?> submit(Runnable task) {};
public <T> Future<T> submit(Runnable task, T result) { };
public <T> Future<T> submit(Callable<T> task) { };
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
};
}
AbstractExecutorService是一個抽象類,它實現了ExecutorService接口。
我們接着看ExecutorService接口的實現:
public interface ExecutorService extends Executor {
void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
而ExecutorService又是繼承了Executor接口,我們看一下Executor接口的實現:
public interface Executor {
void execute(Runnable command);
}
到這裏,大家應該明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor幾個之間的關係了。
Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數爲Runnable類型,從字面意思可以理解,就是用來執行傳進去的任務的;
然後ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法;
然後ThreadPoolExecutor繼承了類AbstractExecutorService。
在ThreadPoolExecutor類中有幾個非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
execute()方法實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執行。
submit()方法是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並沒有對其進行重寫,這個方法也是用來向線程池提交任務的,但是它和execute()方法不同,它能夠返回任務執行的結果,去看submit()方法的實現,會發現它實際上還是調用的execute()方法,只不過它利用了Future來獲取任務執行結果(Future相關內容將在下一篇講述)。
shutdown()和shutdownNow()是用來關閉線程池的。
還有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等獲取與線程池相關屬性的方法,有興趣的朋友可以自行查閱API。
二.深入剖析線程池實現原理
在上一節我們從宏觀上介紹了ThreadPoolExecutor,下面我們來深入解析一下線程池的具體實現原理,將從下面幾個方面講解:
- 1.線程池狀態
- 2.任務的執行
- 3.線程池中的線程初始化
- 4.任務緩存隊列及排隊策略
- 5.任務拒絕策略
- 6.線程池的關閉
- 7.線程池容量的動態調整
1.線程池狀態
在ThreadPoolExecutor中定義了一個Volatile變量,另外定義了幾個static final變量表示線程池的各個狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示當前線程池的狀態,它是一個volatile變量用來保證線程之間的可見性;
下面的幾個static final變量表示runState可能的幾個取值。
當創建線程池後,初始時,線程池處於RUNNING狀態;
如果調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不能夠接受新的任務,它會等待所有任務執行完畢;
如果調用了shutdownNow()方法,則線程池處於STOP狀態,此時線程池不能接受新的任務,並且會去嘗試終止正在執行的任務;
當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態。
2.任務的執行
在瞭解將任務提交給線程池到任務執行完畢整個過程之前,我們先來看一下ThreadPoolExecutor類中其他的一些比較重要成員變量:
private final BlockingQueue<Runnable> workQueue; //任務緩存隊列,用來存放等待執行的任務
private final ReentrantLock mainLock = new ReentrantLock(); //線程池的主要狀態鎖,對線程池狀態(比如線程池大小
//、runState等)的改變都要使用這個鎖
private final HashSet<Worker> workers = new HashSet<Worker>(); //用來存放工作集
private volatile long keepAliveTime; //線程存活時間
private volatile boolean allowCoreThreadTimeOut; //是否允許爲核心線程設置存活時間
private volatile int corePoolSize; //核心池的大小(即線程池中的線程數目大於這個參數時,提交的任務會被放進任務緩存隊列)
private volatile int maximumPoolSize; //線程池最大能容忍的線程數
private volatile int poolSize; //線程池中當前的線程數
private volatile RejectedExecutionHandler handler; //任務拒絕策略
private volatile ThreadFactory threadFactory; //線程工廠,用來創建線程
private int largestPoolSize; //用來記錄線程池中曾經出現過的最大線程數
private long completedTaskCount; //用來記錄已經執行完畢的任務個數
每個變量的作用都已經標明出來了,這裏要重點解釋一下corePoolSize、maximumPoolSize、largestPoolSize三個變量。
corePoolSize在很多地方被翻譯成核心池大小,其實我的理解這個就是線程池的大小。舉個簡單的例子:
假如有一個工廠,工廠裏面有10個工人,每個工人同時只能做一件任務。
因此只要當10個工人中有工人是空閒的,來了任務就分配給空閒的工人做;
當10個工人都有任務在做時,如果還來了任務,就把任務進行排隊等待;
如果說新任務數目增長的速度遠遠大於工人做任務的速度,那麼此時工廠主管可能會想補救措施,比如重新招4個臨時工人進來;
然後就將任務也分配給這4個臨時工人做;
如果說着14個工人做任務的速度還是不夠,此時工廠主管可能就要考慮不再接收新的任務或者拋棄前面的一些任務了。
當這14個工人當中有人空閒時,而新任務增長的速度又比較緩慢,工廠主管可能就考慮辭掉4個臨時工了,只保持原來的10個工人,畢竟請額外的工人是要花錢的。
這個例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是說corePoolSize就是線程池大小,maximumPoolSize在我看來是線程池的一種補救措施,即任務量突然過大時的一種補救措施。
不過爲了方便理解,在本文後面還是將corePoolSize翻譯成核心池大小。
largestPoolSize只是一個用來起記錄作用的變量,用來記錄線程池中曾經有過的最大線程數目,跟線程池的容量沒有任何關係。
下面我們進入正題,看一下任務從提交到最終執行完畢經歷了哪些過程。
在ThreadPoolExecutor類中,最核心的任務提交方法是execute()方法,雖然通過submit也可以提交任務,但是實際上submit方法裏面最終調用的還是execute()方法,所以我們只需要研究execute()方法的實現原理即可:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
上面的代碼可能看起來不是那麼容易理解,下面我們一句一句解釋:
首先,判斷提交的任務command是否爲null,若是null,則拋出空指針異常;
接着是這句,這句要好好理解一下:
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))
由於是或條件運算符,所以先計算前半部分的值,如果線程池中當前線程數不小於核心池大小,那麼就會直接進入下面的if語句塊了。
如果線程池中當前線程數小於核心池大小,則接着執行後半部分,也就是執行:
addIfUnderCorePoolSize(command)
如果執行完addIfUnderCorePoolSize這個方法返回false,則繼續執行下面的if語句塊,否則整個方法就直接執行完畢了。
如果執行完addIfUnderCorePoolSize這個方法返回false,然後接着判斷:
if (runState == RUNNING && workQueue.offer(command))
如果當前線程池處於RUNNING狀態,則將任務放入任務緩存隊列;如果當前線程池不處於RUNNING狀態或者任務放入緩存隊列失敗,則執行:
addIfUnderMaximumPoolSize(command)
如果執行addIfUnderMaximumPoolSize方法失敗,則執行reject()方法進行任務拒絕處理。
回到前面:
if (runState == RUNNING && workQueue.offer(command))
這句的執行,如果說當前線程池處於RUNNING狀態且將任務放入任務緩存隊列成功,則繼續進行判斷:
if (runState != RUNNING || poolSize == 0)
這句判斷是爲了防止在將此任務添加進任務緩存隊列的同時其他線程突然調用shutdown或者shutdownNow方法關閉了線程池的一種應急措施。如果是這樣就執行:
ensureQueuedTaskHandled(command)
進行應急處理,從名字可以看出是保證 添加到任務緩存隊列中的任務得到處理。
我們接着看2個關鍵方法的實現:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask); //創建線程去執行firstTask任務
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
這個是addIfUnderCorePoolSize方法的具體實現,從名字可以看出它的意圖就是當低於核心吃大小時執行的方法。下面看其具體實現,首先獲取到鎖,因爲這地方涉及到線程池狀態的變化,先通過if語句判斷當前線程池中的線程數目是否小於核心池大小,有朋友也許會有疑問:前面在execute()方法中不是已經判斷過了嗎,只有線程池當前線程數目小於核心池大小纔會執行addIfUnderCorePoolSize方法的,爲何這地方還要繼續判斷?原因很簡單,前面的判斷過程中並沒有加鎖,因此可能在execute方法判斷的時候poolSize小於corePoolSize,而判斷完之後,在其他線程中又向線程池提交了任務,就可能導致poolSize不小於corePoolSize了,所以需要在這個地方繼續判斷。然後接着判斷線程池的狀態是否爲RUNNING,原因也很簡單,因爲有可能在其他線程中調用了shutdown或者shutdownNow方法。然後就是執行
t = addThread(firstTask);
這個方法也非常關鍵,傳進去的參數爲提交的任務,返回值爲Thread類型。然後接着在下面判斷t是否爲空,爲空則表明創建線程失敗(即poolSize>=corePoolSize或者runState不等於RUNNING),否則調用t.start()方法啓動線程。
我們來看一下addThread方法的實現:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w); //創建一個線程,執行任務
if (t != null) {
w.thread = t; //將創建的線程的引用賦值爲w的成員變量
workers.add(w);
int nt = ++poolSize; //當前線程數加1
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
在addThread方法中,首先用提交的任務創建了一個Worker對象,然後調用線程工廠threadFactory創建了一個新的線程t,然後將線程t的引用賦值給了Worker對象的成員變量thread,接着通過workers.add(w)將Worker對象添加到工作集當中。
下面我們看一下Worker類的實現:
private final class Worker implements Runnable {
private final ReentrantLock runLock = new ReentrantLock();
private Runnable firstTask;
volatile long completedTasks;
Thread thread;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
boolean isActive() {
return runLock.isLocked();
}
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) {
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
void interruptNow() {
thread.interrupt();
}
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
if (runState < STOP &&
Thread.interrupted() &&
runState >= STOP)
boolean ran = false;
beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor類的一個方法,沒有具體實現,用戶可以根據
//自己需要重載這個方法和後面的afterExecute方法來進行一些統計信息,比如某個任務的執行時間等
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this); //當任務隊列中沒有任務時,進行清理工作
}
}
}
它實際上實現了Runnable接口,因此上面的Thread t = threadFactory.newThread(w);效果跟下面這句的效果基本一樣:
Thread t = new Thread(w);
相當於傳進去了一個Runnable任務,在線程t中執行這個Runnable。
既然Worker實現了Runnable接口,那麼自然最核心的方法便是run()方法了:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
從run方法的實現可以看出,它首先執行的是通過構造器傳進來的任務firstTask,在調用runTask()執行完firstTask之後,在while循環裏面不斷通過getTask()去取新的任務來執行,那麼去哪裏取呢?自然是從任務緩存隊列裏面去取,getTask是ThreadPoolExecutor類中的方法,並不是Worker類中的方法,下面是getTask方法的實現:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果線程數大於核心池大小或者允許爲核心池線程設置空閒時間,
//則通過poll取任務,若等待一定的時間取不到任務,則返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) { //如果沒取到任務,即r爲null,則判斷當前的worker是否可以退出
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers(); //中斷處於空閒狀態的worker
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
在getTask中,先判斷當前線程池狀態,如果runState大於SHUTDOWN(即爲STOP或者TERMINATED),則直接返回null。
如果runState爲SHUTDOWN或者RUNNING,則從任務緩存隊列取任務。
如果當前線程池的線程數大於核心池大小corePoolSize或者允許爲核心池中的線程設置空閒存活時間,則調用poll(time,timeUnit)來取任務,這個方法會等待一定的時間,如果取不到任務就返回null。
然後判斷取到的任務r是否爲null,爲null則通過調用workerCanExit()方法來判斷當前worker是否可以退出,我們看一下workerCanExit()的實現:
private boolean workerCanExit() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
boolean canExit;
//如果runState大於等於STOP,或者任務緩存隊列爲空了
//或者 允許爲核心池線程設置空閒存活時間並且線程池中的線程數目大於1
try {
canExit = runState >= STOP ||
workQueue.isEmpty() ||
(allowCoreThreadTimeOut &&
poolSize > Math.max(1, corePoolSize));
} finally {
mainLock.unlock();
}
return canExit;
}
也就是說如果線程池處於STOP狀態、或者任務隊列已爲空或者允許爲核心池線程設置空閒存活時間並且線程數大於1時,允許worker退出。如果允許worker退出,則調用interruptIdleWorkers()中斷處於空閒狀態的worker,我們看一下interruptIdleWorkers()的實現:
void interruptIdleWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) //實際上調用的是worker的interruptIfIdle()方法
w.interruptIfIdle();
} finally {
mainLock.unlock();
}
}
從實現可以看出,它實際上調用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) { //注意這裏,是調用tryLock()來獲取鎖的,因爲如果當前worker正在執行任務,鎖已經被獲取了,是無法獲取到鎖的
//如果成功獲取了鎖,說明當前worker處於空閒狀態
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
這裏有一個非常巧妙的設計方式,假如我們來設計線程池,可能會有一個任務分派線程,當發現有線程空閒時,就從任務緩存隊列中取一個任務交給空閒線程執行。但是在這裏,並沒有採用這樣的方式,因爲這樣會要額外地對任務分派線程進行管理,無形地會增加難度和複雜度,這裏直接讓執行完任務的線程去任務緩存隊列裏面取任務來執行。
我們再看addIfUnderMaximumPoolSize方法的實現,這個方法的實現思想和addIfUnderCorePoolSize方法的實現思想非常相似,唯一的區別在於addIfUnderMaximumPoolSize方法是在線程池中的線程數達到了核心池大小並且往任務隊列中添加任務失敗的情況下執行的:
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < maximumPoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
看到沒有,其實它和addIfUnderCorePoolSize方法的實現基本一模一樣,只是if語句判斷條件中的poolSize < maximumPoolSize不同而已。
到這裏,大部分朋友應該對任務提交給線程池之後到被執行的整個過程有了一個基本的瞭解,下面總結一下:
1)首先,要清楚corePoolSize和maximumPoolSize的含義;
2)其次,要知道Worker是用來起到什麼作用的;
3)要知道任務提交給線程池之後的處理策略,這裏總結一下主要有4點:
- 如果當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會創建一個線程去執行這個任務;
- 如果當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嘗試創建新的線程去執行這個任務;
- 如果當前線程池中的線程數目達到maximumPoolSize,則會採取任務拒絕策略進行處理;
- 如果線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;如果允許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。
3.線程池中的線程初始化
默認情況下,創建線程池之後,線程池中是沒有線程的,需要提交任務之後纔會創建線程。
在實際中如果需要線程池創建之後立即創建線程,可以通過以下兩個方法辦到:
- prestartCoreThread():初始化一個核心線程;
- prestartAllCoreThreads():初始化所有核心線程
下面是這2個方法的實現:
public boolean prestartCoreThread() {
return addIfUnderCorePoolSize(null); //注意傳進去的參數是null
}
public int prestartAllCoreThreads() {
int n = 0;
while (addIfUnderCorePoolSize(null))//注意傳進去的參數是null
++n;
return n;
}
注意上面傳進去的參數是null,根據第2小節的分析可知如果傳進去的參數爲null,則最後執行線程會阻塞在getTask方法中的
r = workQueue.take();
即等待任務隊列中有任務。
4.任務緩存隊列及排隊策略
在前面我們多次提到了任務緩存隊列,即workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue<Runnable>,通常可以取下面三種類型:
1)ArrayBlockingQueue:基於數組的先進先出隊列,此隊列創建時必須指定大小;
2)LinkedBlockingQueue:基於鏈表的先進先出隊列,如果創建時沒有指定此隊列大小,則默認爲Integer.MAX_VALUE;
3)synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。
5.任務拒絕策略
當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略,通常有以下四種策略:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
6.線程池的關閉
ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:
- shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務
- shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務
7.線程池容量的動態調整
ThreadPoolExecutor提供了動態調整線程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
- setCorePoolSize:設置核心池大小
- setMaximumPoolSize:設置線程池最大能創建的線程數目大小
當上述參數從小變大時,ThreadPoolExecutor進行線程賦值,還可能立即創建新的線程來執行任務。
三.使用示例
前面我們討論了關於線程池的實現原理,這一節我們來看一下它的具體使用:
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+
executor.getQueue().size()+",已執行玩別的任務數目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在執行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"執行完畢");
}
}
執行結果:
正在執行task 0
線程池中線程數目:1,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
線程池中線程數目:2,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 1
線程池中線程數目:3,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 2
線程池中線程數目:4,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 3
線程池中線程數目:5,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0
正在執行task 4
線程池中線程數目:5,隊列中等待執行的任務數目:1,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:2,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:3,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:4,已執行玩別的任務數目:0
線程池中線程數目:5,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
線程池中線程數目:6,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 10
線程池中線程數目:7,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 11
線程池中線程數目:8,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 12
線程池中線程數目:9,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 13
線程池中線程數目:10,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0
正在執行task 14
task 3執行完畢
task 0執行完畢
task 2執行完畢
task 1執行完畢
正在執行task 8
正在執行task 7
正在執行task 6
正在執行task 5
task 4執行完畢
task 10執行完畢
task 11執行完畢
task 13執行完畢
task 12執行完畢
正在執行task 9
task 14執行完畢
task 8執行完畢
task 5執行完畢
task 7執行完畢
task 6執行完畢
task 9執行完畢
從執行結果可以看出,當線程池中線程的數目大於5時,便將任務放入任務緩存隊列裏面,當任務緩存隊列滿了之後,便創建新的線程。如果上面程序中,將for循環中改成執行20個任務,就會拋出任務拒絕異常了。
不過在Javadoc中,並不提倡我們直接使用ThreadPoolExecutor,而是使用Executors類中提供的幾個靜態方法來創建線程池:
Executors.newCachedThreadPool(); //創建一個緩衝池,緩衝池容量大小爲Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //創建容量爲1的緩衝池
Executors.newFixedThreadPool(int); //創建固定容量大小的緩衝池
下面是這三個靜態方法的具體實現:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
從它們的具體實現來看,它們實際上也是調用了ThreadPoolExecutor,只不過參數都已配置好了。
newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置爲1,也使用的LinkedBlockingQueue;
newCachedThreadPool將corePoolSize設置爲0,將maximumPoolSize設置爲Integer.MAX_VALUE,使用的SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過60秒,就銷燬線程。
實際中,如果Executors提供的三個靜態方法能滿足要求,就儘量使用它提供的三個方法,因爲自己去手動配置ThreadPoolExecutor的參數有點麻煩,要根據實際任務的類型和數量來進行配置。
另外,如果ThreadPoolExecutor達不到要求,可以自己繼承ThreadPoolExecutor類進行重寫。
四.如何合理配置線程池的大小
本節來討論一個比較重要的話題:如何合理配置線程池大小,僅供參考。
一般需要根據任務的類型來配置線程池大小:
如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設爲 NCPU+1
如果是IO密集型任務,參考值可以設置爲2*NCPU
當然,這只是一個參考值,具體的設置還需要根據實際情況進行調整,比如可以先將線程池大小設置爲參考值,再觀察任務運行情況和系統負載、資源利用率來進行適當調整。
參考資料:
- http://ifeve.com/java-threadpool/
- http://blog.163.com/among_1985/blog/static/275005232012618849266/
- http://developer.51cto.com/art/201203/321885.htm
- http://blog.csdn.net/java2000_wl/article/details/22097059
- http://blog.csdn.net/cutesource/article/details/6061229
--------------------- 本文來自 不一樣的程序員 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/xiongyouqiang/article/details/79456030?utm_source=copy