Java執行緒池原理淺析
什麼是執行緒池?
為了避免頻繁重複的建立和銷燬執行緒,我們可以讓這些執行緒進行復用,線上程池中,總會有活躍的執行緒在佔用,但是執行緒池中也會存在沒有佔用的執行緒,這些執行緒處於空閒狀態,當有任務的時候會從池子裡面拿去一個執行緒來進行使用,當完成工作後,並沒有銷燬執行緒,而是將將執行緒放回到池子中去。
執行緒池主要解決兩個問題:
一是當執行大量非同步任務時執行緒池能夠提供很好的效能。
二是執行緒池提供了一種資源限制和管理的手段,比如可以限制現成的個數,動態新增執行緒等。
-《Java併發程式設計之美》
上面內容出自《Java併發程式設計之美》這本書,第一個問題上面已經提到過,執行緒的頻繁建立和銷燬是很損耗效能的,但是執行緒池中的執行緒是可以複用的,可以較好的提升效能問題,執行緒池內部是採用了阻塞佇列來維護Runnable物件。
原理分析
JDK為我們封裝了一套操作多執行緒的框架Executors,幫助我們可以更好的控制執行緒池,Executors下提供了一些執行緒池的工廠方法:
- newFixedThreadPool:返回固定長度的執行緒池,執行緒池中的執行緒數量是固定的。
- newCacheThreadPool:該方法返回一個根據實際情況來進行調整執行緒數量的執行緒池,空餘執行緒存活時間是60s
- newSingleThreadExecutor:該方法返回一個只有一個執行緒的執行緒池。
- newSingleThreadScheduledExecutor:該方法返回一個
SchemeExecutorService
物件,執行緒池大小為1,SchemeExecutorService
ThreadPoolExecutor
類和ExecutorService
介面之上的擴充套件,在給定時間執行某任務。 - newSchemeThreadPool:該方法返回一個
SchemeExecutorService
物件,可指定執行緒池執行緒數量。
對於核心的執行緒池來說,它內部都是使用了ThreadPoolExecutor
物件來實現的,只不過內部引數資訊不一樣,我們先來看兩個例子:nexFixedThreadPool
和newSingleThreadExecutor
如下所示:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); } public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
由上面的執行緒池的建立過程可以看到它們都是ThreadPoolExecutor
的封裝,接下來我們來看一下ThreadPoolExecutor
的引數說明:
引數名稱 | 引數描述 |
---|---|
corePoolSize | 指定執行緒池執行緒的數量 |
maximumPoolSize | 指定執行緒池中執行緒的最大數量 |
keepAliveTime | 當執行緒池執行緒的數量超過corePoolSize的時候,多餘的空閒執行緒存活的時間,如果超過了corePoolSize,在keepAliveTime的時間之後,銷燬執行緒 |
unit | keepAliveTime的單位 |
workQueue | 工作佇列,將被提交但尚未執行的任務快取起來 |
threadFactory | 執行緒工廠,用於建立執行緒,不指定為預設執行緒工廠DefaultThreadFactory |
handler | 拒絕策略 |
其中workQueue代表的是提交但未執行的佇列,它是BlockingQueue介面的物件,用於存放Runable物件,主要分為以下幾種型別:
直接提交的佇列:
SynchronousQueue
佇列,它是一個沒有容量的佇列,前面我有對其進行講解,當執行緒池進行入隊offer操作的時候,本身是無容量的,所以直接返回false,並沒有儲存下來,而是直接提交給執行緒來進行執行,如果沒有空餘的執行緒則執行拒絕策略。有界的任務佇列:可以使用
ArrayBlockingQueue
佇列,因為它內部是基於陣列來進行實現的,初始化時必須指定容量引數,當使用有界任務佇列時,當有任務進行提交時,執行緒池的執行緒數量小於corePoolSize則建立新的執行緒來執行任務,當執行緒池的執行緒數量大於corePoolSize的時候,則將提交的任務放入到佇列中,當提交的任務塞滿佇列後,如果執行緒池的執行緒數量沒有超過maximumPoolSize,則建立新的執行緒執行任務,如果超過了maximumPoolSize則執行拒絕策略。- 無界的任務佇列:可以使用
LinkedBlockingQueue
佇列,它內部是基於連結串列的形式,預設佇列的長度是Integer.MAX_VALUE
,也可以指定佇列的長度,當佇列滿時進行阻塞操作,當然執行緒池中採用的是offer
方法並不會阻塞執行緒,當佇列滿時則返回false,入隊成功則則返回true,當使用LinkedBlockingQueue
佇列時,有任務提交到執行緒池時,如果執行緒池的數量小於corePoolSize,執行緒池會產生新的執行緒來執行任務,當執行緒池的執行緒數量大於corePoolSize時,則將提交的任務放入到佇列中,等待執行任務的執行緒執行完之後進行消費佇列中的任務,若後續仍有新的任務提交,而沒有空閒的執行緒時,它會不斷往佇列中入隊提交的任務,直到資源耗盡。 優先任務佇列:t有限任務佇列是帶有執行優先順序的佇列,他可以使用
PriorityBlockingQueue
佇列,可以控制任務的執行先後順序,它是一個無界佇列,該佇列可以根據任務自身的優先順序順序先後執行,在確保效能的同時,也能有很好的質量保證。
上面講解了關於執行緒池內部都是通過ThreadPoolExecutor
來進行實現的,那麼下面我以一個例子來進行原始碼分析:
public class ThreadPoolDemo1 {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(5), new CustomThreadFactory());
for (int i = 0; i < 15; i++) {
executorService.execute(() -> {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("由執行緒:" + Thread.currentThread().getName() + "執行任務完成");
});
}
}
}
上面定義了一個執行緒池,執行緒池初始化的corePoolSize為5,也就是執行緒池中執行緒的數量為5,最大執行緒maximumThreadPoolSize為10,空餘的執行緒存活的時間是60s,使用LinkedBlockingQueue來作為阻塞佇列,這裡還發現我自定義了ThreadFactory
執行緒池工廠,這裡我真是針對執行緒建立的時候輸出執行緒池的名稱,原始碼如下所示:
/**
* 自定義的執行緒池構造工廠
*/
public class CustomThreadFactory 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;
public CustomThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
@Override
public Thread newThread(Runnable r) {
String name = namePrefix + threadNumber.getAndIncrement();
Thread t = new Thread(group, r,
name,
0);
System.out.println("執行緒池建立,執行緒名稱為:" + name);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
程式碼和DefaultThreadFactory
一樣,只是在newThread
新建執行緒的動作的時候輸出了執行緒池的名稱,方便檢視執行緒建立的時機,上面main
方法中提交了15個任務,呼叫了execute
方法來進行提交任務,在分析execute
方法之前我們先了解一下執行緒的狀態:
//假設Integer型別是32位的二進位制表示。
//高3位代表執行緒池的狀態,低29位代表的是執行緒池的數量
//預設是RUNNING狀態,執行緒池的數量為0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//執行緒個數位數,表示的Integer中除去最高的3位之後剩下的位數表示執行緒池的個數
private static final int COUNT_BITS = Integer.SIZE - 3;
//執行緒池的執行緒的最大數量
//這裡舉例是32為機器,表示為00011111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//執行緒池的狀態
// runState is stored in the high-order bits
//11100000000000000000000000000000
//接受新任務並且處理阻塞佇列裡面任務
private static final int RUNNING = -1 << COUNT_BITS;
//00000000000000000000000000000000
//拒絕新任務但是處理阻塞佇列的任務
private static final int SHUTDOWN = 0 << COUNT_BITS;
//00100000000000000000000000000000
//拒接新任務並且拋棄阻塞佇列裡面的任務,同時會中斷正在處理的任務
private static final int STOP = 1 << COUNT_BITS;
//01000000000000000000000000000000
//所有任務都執行完(包括阻塞佇列中的任務)後當執行緒池活動執行緒數為0,將要呼叫terminated方法。
private static final int TIDYING = 2 << COUNT_BITS;
//01100000000000000000000000000000
//終止狀態,terminated方法呼叫完成以後的狀態
private static final int TERMINATED = 3 << COUNT_BITS;
通過上面內容可以看到ctl其實存放的是執行緒池的狀態和執行緒數量的變數,預設是RUNNING
,也就是11100000000000000000000000000000
,這裡我們來假設執行的機器上的Integer的是32位的,因為有些機器上可能Integer並不是32位,下面COUNT_BITS來控制位數,也就是先獲取Integer在該平臺上的位數,比如說是32位,然後32位-3位=29位,也就是低29位代表的是現成的數量,高3位代表執行緒的狀態,可以清晰看到下面的執行緒池的狀態都是通過低位來進行向左位移的操作的,除了上面的變數,還提供了操作執行緒池狀態的方法:
// 操作ctl變數,主要是進行分解或組合執行緒數量和執行緒池狀態。
// 獲取高3位,獲取執行緒池狀態
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 獲取低29位,獲取執行緒池中執行緒的數量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 組合ctl變數,rs=runStatue代表的是執行緒池的狀態,wc=workCount代表的是執行緒池執行緒的數量
private static int ctlOf(int rs, int wc) { return rs | wc; }
/*
* Bit field accessors that don't require unpacking ctl.
* These depend on the bit layout and on workerCount being never negative.
*/
//指定的執行緒池狀態c小於狀態s
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
//指定的執行緒池狀態c至少是狀態s
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
// 判斷執行緒池是否執行狀態
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
/**
* CAS增加執行緒池執行緒數量.
*/
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
/**
* CAS減少執行緒池執行緒數量
*/
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
/**
* 將執行緒池的執行緒數量進行較少操作,如果競爭失敗直到競爭成功為止。
*/
private void decrementWorkerCount() {
do {} while (! compareAndDecrementWorkerCount(ctl.get()));
}
下來我們看一下ThreadPoolExecutor
物件下的execute
方法:
public void execute(Runnable command) {
// 判斷提交的任務是不是為空,如果為空則丟擲NullPointException異常
if (command == null)
throw new NullPointerException();
// 獲取執行緒池的狀態和執行緒池的數量
int c = ctl.get();
// 如果執行緒池的數量小於corePoolSize,則進行新增執行緒執行任務
if (workerCountOf(c) < corePoolSize) {
//新增執行緒修改執行緒數量並且將command作為第一個任務進行處理
if (addWorker(command, true))
return;
// 獲取最新的狀態
c = ctl.get();
}
// 如果執行緒池的狀態是RUNNING,將命令新增到佇列中
if (isRunning(c) && workQueue.offer(command)) {
//二次檢查執行緒池狀態和執行緒數量
int recheck = ctl.get();
//執行緒不是RUNNING狀態,從佇列中移除當前任務,並且執行拒絕策略。
//這裡說明一點,只有RUNNING狀態的執行緒池才會接受新的任務,其餘狀態全部拒絕。
if (! isRunning(recheck) && remove(command))
reject(command);
//如果執行緒池的執行緒數量為空時,代表執行緒池是空的,新增一個新的執行緒。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果佇列是滿的,或者是SynchronousQueue佇列時,則直接新增新的執行緒執行任務,如果新增失敗則進行拒絕
//可能執行緒池的執行緒數量大於maximumPoolSize則採取拒絕策略。
else if (!addWorker(command, false))
reject(command);
}
通過分析execute方法總結以下幾點:
- 當執行緒池中執行緒的數量小於
corePoolSize
時,直接新增執行緒到執行緒池並且將當前任務做為第一個任務執行。 - 如果執行緒池的狀態的是
RUNNING
,則可以接受任務,將任務放入到阻塞佇列中,內部進行二次檢查,有可能在執行下面內容時執行緒池狀態已經發生了變化,在這個時候如果執行緒池狀態變成不是RUNNING
,則將當前任務從佇列中移除,並且進行拒絕策略。 - 如果阻塞佇列已經滿了或者
SynchronousQueue
這種特殊佇列無空間的時候,直接新增新的執行緒執行任務,當執行緒池的執行緒數量大於maximumPoolSize
時相應拒絕策略。 - 入隊操作用的是
offer
方法,該方法不會阻塞佇列,如果佇列已經滿時或超時導致入隊失敗,返回false,如果入隊成功返回true。
針對上面例子原始碼我們來做一下分析,我們原始碼中阻塞佇列採用的是ArrayBlockingQueue
佇列,並且指定佇列的長度是5,我們看下面提交的執行緒池的任務是15個,而且corePoolSize設定的是5個核心執行緒,最大執行緒數(maximumPoolSzie)是10個(包括核心執行緒數),假設所有任務都同時提交到了執行緒池中,其中有5個任務會被提交到執行緒中作為第一個任務進行執行,會有5個任務被新增到阻塞佇列中,還有5個任務提交到到執行緒池中的時候發現阻塞佇列已經滿了,這時候會直接提交任務,發現當前執行緒數是5小於最大執行緒數,可以進行新建執行緒來執行任務。
這裡我們只是假設任務全部提交,因為我們在任務中添加了Thread.sleep睡眠一會,在for迴圈結束提交任務之後可能才會結束掉任務的睡眠執行任務後面內容,所以可以看做是全部提交任務,但是沒有任務完成,如果有任務完成的話,可能就不會是觸發最大的執行緒數,有可能就是一個任務完成後從佇列取出來,然後另一個任務來的時候可以新增到佇列中,上圖中可以看到,有5個核心core執行緒在執行任務,任務佇列中有5個任務在等待空餘執行緒執行,而還有5個正在執行的執行緒,核心執行緒是指在corePoolSize範圍的執行緒,而非核心執行緒指的是大於corePoolSize但是小於等於MaximumPoolSize的執行緒,就是這些非核心執行緒並不是一直存活的執行緒,它會跟隨執行緒池指定的引數來進行銷燬,我們這裡指定了60s後如果沒有任務提交,則會進行銷燬操作,當然工作執行緒並不指定那些執行緒必須回收那些執行緒就必須保留,是根據從佇列中獲取任務來決定,如果執行緒獲取任務時發現執行緒池中的執行緒數量大於corePoolSize,並且阻塞佇列中為空時,則阻塞佇列會阻塞60s後如果還有沒有任務就返回false,這時候會釋放執行緒,呼叫processWorkerExit
來處理執行緒的退出,接下來我們來分析下addWorker
都做了什麼內容:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//獲取執行緒池的狀態和執行緒池執行緒的數量
int c = ctl.get();
//單獨獲取執行緒池的狀態
int rs = runStateOf(c);
//檢查佇列是否只在必要時為空
if (rs >= SHUTDOWN && //執行緒池的狀態是SHUTDOWN、STOP、TIDYING、TERMINATED
! (rs == SHUTDOWN && //可以看做是rs!=SHUTDOWN,執行緒池狀態為STOP、TIDYING、TERMINATED
firstTask == null && //可以看做firstTask!=null,並且rs=SHUTDOWN
! workQueue.isEmpty())) //可以看做rs=SHUTDOWN,並且workQueue.isEmpty()佇列為空
return false;
//迴圈CAS增加執行緒池中執行緒的個數
for (;;) {
//獲取執行緒池中執行緒個數
int wc = workerCountOf(c);
//如果執行緒池執行緒數量超過最大執行緒池數量,則直接返回
if (wc >= CAPACITY ||
//如果指定使用corePoolSize作為限制則使用corePoolSize,反之使用maximumPoolSize,最為工作執行緒最大執行緒執行緒數量,如果工作執行緒大於相應的執行緒數量則直接返回。
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS增加執行緒池中執行緒的數量
if (compareAndIncrementWorkerCount(c))
//跳出增加執行緒池數量。
break retry;
//如果修改失敗,則重新獲取執行緒池的狀態和執行緒數量
c = ctl.get(); // Re-read ctl
//如果最新的執行緒池狀態和原有縣城出狀態不一樣時,則跳轉到外層retry中,否則在內層迴圈重新進行CAS
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//工作執行緒是否開始啟動標誌
boolean workerStarted = false;
//工作執行緒新增到執行緒池成功與否標誌
boolean workerAdded = false;
Worker w = null;
try {
//建立一個Worker物件
w = new Worker(firstTask);
//獲取worker中的執行緒,這裡執行緒是通過ThreadFactory執行緒工廠創建出來的,詳細看下面原始碼資訊。
final Thread t = w.thread;
//判斷執行緒是否為空
if (t != null) {
//新增獨佔鎖,為新增worker進行同步操作,防止其他執行緒同時進行execute方法。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//獲取執行緒池的狀態
int rs = runStateOf(ctl.get());
//如果執行緒池狀態為RUNNING或者是執行緒池狀態為SHUTDOWN並且第一個任務為空時,當執行緒池狀態為SHUTDOWN時,是不允許新增新任務的,所以他會從佇列中獲取任務。
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//新增worker到集合中
workers.add(w);
int s = workers.size();
//跟蹤最大的執行緒池數量
if (s > largestPoolSize)
largestPoolSize = s;
//新增worker成功
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//如果新增worker成功就啟動任務
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//如果沒有啟動,w不為空就已出worker,並且執行緒池數量進行減少。
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
通過上面addWorker
方法可以分為兩個部分來進行講解,第一部分是對執行緒池中執行緒數量的通過CAS的方式進行增加,其中第一部分中上面有個if語句,這個地方著重分析下:
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
可以看成下面的樣子,將!
放到括號裡面,變成下面的樣子:
if (rs >= SHUTDOWN &&
(rs != SHUTDOWN ||
firstTask != null ||
workQueue.isEmpty()))
return false;
- 執行緒池的狀態是SHUTDOWN、STOP、TIDYING、TERMINATED
- 當執行緒池狀態是STOP、TIDYING、TERMINATED時,這些狀態的時候不需要進行執行緒的新增和啟動操作,因為如果是上面的狀態,其實執行緒池的執行緒正在進行銷燬操作,意味著執行緒呼叫了shutdownNow等方法。
- 如果執行緒池狀態為SHUTDOWN並且第一個任務不為空時,不接受新的任務,直接返回false,也就是說SHUTDOWN的狀態,不會接受新任務,只會針對佇列中未完成的任務進行操作。
- 當線執行緒池狀態為SHUTDOWN並且佇列為空時,直接返回不進行任務新增。
上半部分分為內外兩個迴圈,外迴圈對執行緒池狀態的判斷,用於判斷是否需要新增工作任務執行緒,通過上面講的內容進行判斷,後面內迴圈則是通過CAS操作增加執行緒數,如果指定了core
引數為true,代表執行緒池中執行緒的數量沒有超過corePoolSize
,當指定為false時,代表執行緒池中執行緒數量達到了corePoolSize
,並且佇列已經滿了,或者是SynchronousQueue
這種無空間的佇列,但是還沒有達到最大的執行緒池maximumPoolSize
,所以它內部會根據指定的core
引數來判斷是否已經超過了最大的限制,如果超過了就不能進行新增執行緒了,並且進行拒絕策略,如果沒有超過就增加執行緒數量。
第二部分主要是把任務新增到worker中,並啟動執行緒,這裡我們先來看一下Worker物件。
// 這裡發現它是實現了AQS,是一個不可重入的獨佔鎖模式
// 並且它還集成了Runable介面,實現了run方法。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
/** 執行任務的執行緒,通過ThreadFactory建立 */
final Thread thread;
/** 初始化第一個任務*/
Runnable firstTask;
/** 每個執行緒完成任務的數量 */
volatile long completedTasks;
/**
* 首先現將state值設定為-1,因為在AQS中state=0代表的是鎖沒有被佔用,而且線上程池中shutdown方法會判斷能否爭搶到鎖,如果可以獲得鎖則對執行緒進行中斷操作,如果呼叫了shutdownNow它會判斷state>=0會被中斷。
* firstTask第一個任務,如果為空則會從佇列中獲取任務,後面runWorker中。
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** 委託呼叫外部的runWorker方法 */
public void run() {
runWorker(this);
}
//是否獨佔鎖
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
//這裡就是上面shutdownNow中呼叫的執行緒中斷的方法,getState()>=0
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
可以看到Worker是一個實現了AQS的鎖,它是一個不可重入的獨佔鎖,並且他也實現了Runnable
介面,實現了run
方法,在建構函式中將AQS的state
設定為-1
,為了避免執行緒還沒有進入runWorker
方法前,就呼叫了shutdown
或shutdownNow
方法,會被中斷,設定為-1則不會被中斷。後面我們看到run
方法,它呼叫的是ThreadPoolExecutor
的runWorker
方法,我們這裡回想一下,在addWorker
方法中,新增worker
到HashSet<Worker>
中後,他會將workerAdded
設定為true,代表新增worker
成功,後面有呼叫了下面程式碼:
if (workerAdded) {
t.start();
workerStarted = true;
}
這個t代表的就是在Worker建構函式中的使用ThreadFactory
建立的執行緒,並且將自己(Worker自己)傳遞了當前執行緒,建立的執行緒就是任務執行緒,任務執行緒啟動的時候會呼叫Worker
下的run
方法,run
方法內部又委託給外部方法runWorker
來進行操作,它的引數傳遞的是呼叫者自己,Worker
中的run
方法如下所示:
public void run() {
runWorker(this); //this指Worker物件本身
}
這裡簡單畫一張圖來表示下呼叫的邏輯。
整體的邏輯是先進行建立執行緒,執行緒將Worker
設定為執行程式,並將執行緒塞到Worker
中,然後再addWorker中將Worker中的執行緒取出來,進行啟動操作,啟動後他會呼叫Worker中的run方法,然後run方法中將呼叫ThreadPoolExecutor的runWorker,然後runWorker又會呼叫Worker中的任務firstTask,這個fistTask是要真正執行的任務,也是使用者自己實現的程式碼邏輯。
接下來我們就要看一下runWorker方法裡面具體內容:
final void runWorker(Worker w) {
//呼叫者也就是Worker中的執行緒
Thread wt = Thread.currentThread();
//獲取Worker中的第一個任務
Runnable task = w.firstTask;
//將Worker中的任務清除代表執行了第一個任務了,後面如果再有任務就從佇列中獲取。
w.firstTask = null;
//這裡還記的我們在new Worker的時候將AQS的state狀態設定為-1,這裡先進行解鎖操作,將state設定為0
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//迴圈進行獲取任務,如果第一個任務不為空,或者是如果第一個任務為空,從任務佇列中獲取任務,如果有任務則返回獲取的任務資訊,如果沒有任務可以獲取則進行阻塞,阻塞也分兩種第一種是阻塞直到任務佇列中有內容,第二種是阻塞佇列一定時間之後還是沒有任務就直接返回null。
while (task != null || (task = getTask()) != null) {
//先獲取worker的獨佔鎖,防止其他執行緒呼叫了shutdown方法。
w.lock();
// 如果執行緒池正在停止,確保執行緒是被中斷的,如果沒有則確保執行緒不被中斷操作。
if ((runStateAtLeast(ctl.get(), STOP) || //如果執行緒池狀態為STOP、TIDYING、TERMINATED直接拒絕任務中斷當前執行緒
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//執行任務之前做一些操作,可進行自定義
beforeExecute(wt, task);
Throwable thrown = null;
try {
//執行任務在這裡嘍。
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//執行任務之後做一些操作,可進行自定義
afterExecute(task, thrown);
}
} finally {
//將任務清空為了下次任務獲取
task = null;
//統計當前Worker完成了多少任務
w.completedTasks++;
//獨佔鎖釋放
w.unlock();
}
}
completedAbruptly = false;
} finally {
//處理Worker的退出操作,執行清理工作。
processWorkerExit(w, completedAbruptly);
}
}
我們看到如果Worker是第一次被啟動,它會從Worker中獲取firstTask任務來執行,然後執行成功後,它會getTask()來從佇列中獲取任務,這個地方比較有意思,它是分情況進行獲取任務的,我們都直到BlockingQueue中提供了幾種從佇列中獲取的方法,這個getTask中使用了兩種方式,第一種是使用poll進行獲取佇列中的資訊,它採用的是過一點時間如果佇列中仍沒有任務時直接返回null,然後還有一個就是take方法,take方法是如果佇列中沒有任務則將當前執行緒進行阻塞,等待佇列中有任務後,會通知等待的佇列執行緒進行消費任務,讓我們看一下getTask方法:
private Runnable getTask() {
boolean timedOut = false; //poll獲取超時
for (;;) {
//獲取執行緒池的狀態和執行緒數量
int c = ctl.get();
//獲取執行緒池的狀態
int rs = runStateOf(c);
//執行緒池狀態大於等於SHUTDOWN
//1.執行緒池如果是大於STOP的話減少工作執行緒池數量
//2.如果執行緒池狀態為SHUTDOW並且佇列為空時,代表隊列任務已經執行完,返回null,執行緒數量減少1
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//獲取執行緒池數量。
int wc = workerCountOf(c);
//如果allowCoreThreadTimeOut為true,則空閒執行緒在一定時間未獲得任務會清除
//或者如果執行緒數量大於corePoolSize的時候會進行清除空閒執行緒
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//1.如果執行緒池數量大於最大的執行緒池數量或者對(空餘執行緒進行清除操作並且poll超時了,意思是佇列中沒有內容了,導致poll間隔一段時間後沒有獲取內容超時了。
//2.如果執行緒池的數量大於1或者是佇列已經是空的
//總之意思就是當執行緒池的執行緒池數量大於corePoolSize,或指定了allowCoreThreadTimeOut為true,當佇列中沒有資料或者執行緒池數量大於1的情況下,嘗試對執行緒池的數量進行減少操作,然後返回null,用於上一個方法進行清除操作。
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//如果timed代表的是清除空閒執行緒的意思
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //等待一段時間如果沒有獲取到返回null。
workQueue.take(); //阻塞當前執行緒
//如果佇列中獲取到內容則返回
if (r != null)
return r;
//如果沒有獲取到超時了則設定timeOut狀態
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
- 工作執行緒呼叫getTask從佇列中進行獲取任務。
- 如果指定了allowCoreThreadTimeOut或執行緒池執行緒數量大於corePoolSize則進行清除空閒多餘的執行緒,呼叫阻塞佇列的poll方法,在指定時間內如果沒有獲取到任務直接返回false。
- 如果執行緒池中執行緒池數量小於corePoolSize或者allowCoreThreadTimeOut為false預設值,則進行阻塞執行緒從佇列中獲取任務,直到佇列有任務喚醒執行緒。
我們還記得第一張圖中有標記出來是core執行緒和普通執行緒,其實這樣標記不是很準確,準確的意思是如果執行緒池的數量超過了corePoolSize並且沒有特別指定allowCoreThreadTimeOut的情況下,它會清除掉大於corePoolSize並且小於等於maximumPoolSize的一些執行緒,標記出core執行緒的意思是有corePoolSize不會被清除,但是會清除大於corePoolSize的執行緒,也就是執行緒池中的執行緒對獲取任務的時候進行判斷,也就是getTask中進行判斷,如果當前執行緒池的執行緒數量大於corePoolSize就使用poll方式獲取佇列中的任務,當過一段時間還沒有任務就會返回null,返回null之後設定timeOut=true,並且獲取getTask也會返回null,到此會跳到呼叫者runWorker方法中,一直在while (task != null || (task = getTask()) != null)
此時的getTask返回null跳出while迴圈語句,設定completedAbruptly = false,表示不是突然完成的而是正常完成,退出後它會執行finally的processWorkerExit(w, completedAbruptly)
,執行清理工作。我們來看下原始碼:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // 如果突然完成則調整執行緒數量
decrementWorkerCount(); // 減少執行緒數量1
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //獲取鎖,同時只有一個執行緒獲得鎖
try {
completedTaskCount += w.completedTasks; //統計整個執行緒池完成的數量
workers.remove(w); //將完成任務的worker從HashSet中移除
} finally {
mainLock.unlock(); //釋放鎖
}
//嘗試設定執行緒池狀態為TERMINATED
//1.如果執行緒池狀態為SHUTDOWN並且執行緒池執行緒數量與工作佇列為空時,修改狀態。
//2.如果執行緒池狀態為STOP並且執行緒池執行緒數量為空時,修改狀態。
tryTerminate();
// 獲取執行緒池的狀態和執行緒池的數量
int c = ctl.get();
// 如果執行緒池的狀態小於STOP,也就是SHUTDOWN或RUNNING狀態
if (runStateLessThan(c, STOP)) {
//如果不是突然完成,也就是正常結束
if (!completedAbruptly) {
//如果指定allowCoreThreadTimeOut=true(預設false)則代表執行緒池中有空餘執行緒時需要進行清理操作,否則執行緒池中的執行緒應該保持corePoolSize
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
//這裡判斷如果執行緒池中佇列為空並且執行緒數量最小為0時,將最小值調整為1,因為佇列中還有任務沒有完成需要增加佇列,所以這裡增加了一個執行緒。
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
//如果當前執行緒數效益核心個數,就增加一個Worker
addWorker(null, false);
}
通過上面的原始碼可以得出,如果執行緒數超過核心執行緒數後,在runWorker
中就不會等待佇列中的訊息,而是會進行清除操作,上面的清除程式碼首先是先對執行緒池的數量進行較少操作,其次是統計整個執行緒池中完成任務的數量,然後就是嘗試修改執行緒池的狀態由SHUTDOWN->TIDYING->TERMINATED
或者是由STOP->TIDYING->TERMINATED
,修改執行緒池狀態為TERMINATED
,需要有兩個條件:
當執行緒池執行緒數量和工作佇列為空,並且執行緒池的狀態為
SHUTDOWN
時,才會將狀態進行修改,修改的過程是SHUTDOWN->TIDYING->TERMINATED
當執行緒池的狀態為
STOP
並且執行緒池數量為空時,才會嘗試修改狀態,修改過程是STOP->TIDYING->TERMINATED
如果設定為TERMINATED
狀態,還需要呼叫條件變數termination
的signalAll()
方法來喚醒所有因為呼叫awaitTermination
方法而被阻塞的執行緒,換句話說當呼叫awaitTermination
後,只有執行緒池狀態變成TERMINATED才會被喚醒。
接下來我們就來分析一下這個tryTerminate
方法,看一下他到底符不符合我們上述說的內容:
final void tryTerminate() {
for (;;) {
// 獲取執行緒池的狀態和執行緒池的數量組合狀態
int c = ctl.get();
//這裡單獨下面進行分析,這裡說明兩個問題,需要反向來想這個問題。
//1.如果執行緒池狀態STOP則不進入if語句
//2.如果執行緒池狀態為SHUTDOWN並且工作佇列為空時,不進入if語句
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//如果執行緒池數量不為空時,進行中斷操作。
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//修改狀態為TIDYING,並且將執行緒池的數量進行清空
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//執行一些邏輯,預設是空的
terminated();
} finally {
//修改狀態為TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
//喚醒呼叫awaitTermination方法的執行緒
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
我們單獨將上面的if語句摘出來進行分析,將上面的第一個if判斷進行修改如下,可以看到return在else裡面,這時候內部if判斷進行轉換,轉換成如下所示:
if (!isRunning(c) &&
!runStateAtLeast(c, TIDYING) && //只能是SHUTDOWN和STOP
(runStateOf(c) != SHUTDOWN || workQueue.isEmpty())){
//這裡執行邏輯
}else {
return;
}
逐一分析分析內容如下:
!isRunning(c)
代表不是RUNNING,則可能的是SHUTDOWN
,STOP
,TIDYING
,TERMINATED
這四種狀態- 中間的連線符是並且的意思,跟著
runStateAtLeast(c, TIDYING)
這句話的意思是至少是TIDYING
,TERMINATED
這兩個,反過來就是可能是RUNNING
,SHUTDOWN
,STOP
,但是前面已經判斷了不能是RUNINNG
狀態,所以前面兩個連在一起就是隻能是狀態為SHUTDOWN
,STOP
runStateOf(c) != SHUTDOWN || workQueue.isEmpty()
當前面的狀態是SHUTDOWN
時,則會出發workQueue.isEmpty()
,連在一起就是狀態是SHUTDOWN
並工作佇列為空,當執行緒池狀態為STOP
時,則會進入到runStateOf(c) != SHUTDOWN
,直接返回true,就代表執行緒池狀態為STOP
後面還有一個語句一個if語句將其轉換一下邏輯就是下面的內容:
if (workerCountOf(c) == 0) {
//執行下面的邏輯
}else{
interruptIdleWorkers(ONLY_ONE);
return;
}
這裡我們也進行轉換下,就可以看出來當執行緒池的數量為空時,才會進行下面的邏輯,下面的邏輯就是修改執行緒池狀態為TERMINATED
,兩個連在一起就是上面分析的修改狀態為TERMINATED
的條件,這裡畫一張圖來表示執行緒池狀態的資訊:
其實上面圖中我們介紹了關於從SHUTDOWN
或STOP
到TERMINATED
的變化,沒有講解關於如何從RUNNING
狀態轉變成SHUTDOWN
或STOP
狀態,其實是呼叫了shutdown()
或shutdownNow
方法對其進行狀態的變換,下面來看一下shutdown
方法原始碼:
public void shutdown() {
//獲取全域性鎖
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//許可權檢查
checkShutdownAccess();
//設定執行緒池狀態為SHUTDOWN,如果狀態已經是大於等於SHUTDOWN則直接返回
advanceRunState(SHUTDOWN);
//如果執行緒沒有設定中斷標識並且執行緒沒有執行則設定中斷標識
interruptIdleWorkers();
//空的可以實現的內容
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//嘗試修改執行緒池狀態為TERMINATED
tryTerminate();
}
- 首先對當前執行緒進行許可權檢測,檢視是否設定了安全管理器,如果設定了則要看當前呼叫shutdown的執行緒有沒有許可權都關閉執行緒的許可權,如果有許可權還要看是否有中斷工作現成的許可權,如果沒有許可權則丟擲
SecurityException
或NullPointException
異常。 - 設定執行緒池狀態為SHUTDOWN,如果狀態已經是大於等於SHUTDOWN則直接返回
- 如果執行緒沒有設定中斷標識並且執行緒沒有執行則設定中斷標識
- 嘗試修改執行緒池狀態為TERMINATED
接下來我們來看一下advanceRunState
內容如下所示:
private void advanceRunState(int targetState) {
for (;;) {
//獲取執行緒池狀態和執行緒池的執行緒數量
int c = ctl.get();
if (runStateAtLeast(c, targetState) || //如果執行緒池的狀態>=SHUTDOWN
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) //設定執行緒池狀態為SHUTDOWN
//返回
break;
}
}
- 當執行緒池的狀態>=SHUTDOWN,直接返回
- 如果執行緒池狀態為RUNNING,設定執行緒池狀態為SHUTDOWN,設定成功則返回
interruptIdleWorkers
程式碼如下所示:
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
//獲取全域性鎖,同時只能有一個執行緒能夠呼叫shutdown方法
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//遍歷工作執行緒
for (Worker w : workers) {
Thread t = w.thread;
//如果當前執行緒沒有設定中斷標誌並且可以獲取Worker自己的鎖
if (!t.isInterrupted() && w.tryLock()) {
try {
//設定中斷標誌
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
//執行一次,清理空閒執行緒。
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
我們看到當我們呼叫shutdown方法的時候,只是將空閒的執行緒給設定了中斷標識,也就是活躍正在執行任務的執行緒並沒有設定中斷標識,直到將任務全部執行完後才會逐步清理執行緒操作,我們還記的在getTask中的方法裡面有這樣一段程式碼:
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
判斷是否是狀態>=SHUTDOWN,並且佇列為空時,將執行緒池數量進行減少操作,內部進行CAS操作,直到CAS操作成功為止,並且返回null,返回null後,會呼叫processWorkerExit(w, false);
清理Workers執行緒資訊,並且嘗試將執行緒設定為TERMINATED
狀態,上面是對所有shutdown
方法的分析,下面來看一下shutdownNow
方法並且比較兩個之間的區別:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//許可權檢查
checkShutdownAccess();
//設定執行緒池狀態為STOP,如果狀態已經是大於等於STOP則直接返回
advanceRunState(STOP);
//這裡是和SHUTDOWN區別的地方,這裡是強制進行中斷操作
interruptWorkers();
//將為完成任務複製到list集合中
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//嘗試修改執行緒池狀態為TERMINATED
tryTerminate();
return tasks;
}
shutdownNow
方法返回了未完成的任務資訊列表tasks = drainQueue();
,其實該方法和shutdown
方法主要的區別在於一下幾點內容:
shutdownNow
方法將執行緒池狀態設定為STOP
,而shutdown
則將狀態修改為SHUTDOWN
shutdownNow
方法將工作任務進行中斷操作,也就是說如果工作執行緒在工作也會被中斷,而shutdown
則是先嚐試獲取鎖如果獲得鎖成功則進行中斷標誌設定,也就是中斷操作,如果沒有獲取到鎖則等待進行完成後自動退出。shutdownNow
方法返回未完成的任務列表。
下面程式碼是shutDownNow
的interruptWorkers
方法:
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
//直接進行中斷操作。
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
內部呼叫了Worker
的interruptIfStarted
方法,方法內部是針對執行緒進行中斷操作,但是中斷的前提條件是AQS的state狀態必須大於等於0,如果狀態為-1的則不會被中斷,但是如果任務執行起來的時候在runWorker
中則不會執行任務,因為執行緒池狀態為STOP
,如果執行緒池狀態為STOP則會中斷執行緒,下面程式碼是Worker中的interruptIfStarted
:
void interruptIfStarted() {
Thread t;
//當前Worker鎖狀態大於等於0並且執行緒沒有被中斷
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
拒絕策略
JDK內建的拒絕策略如下:
- AbortPolicy策略:該策略會直接丟擲異常,阻止系統正常工作
- CallerRunsPolicy策略:只要執行緒池沒有關閉執行緒池狀態是RUNNING狀態,該略略直接呼叫執行緒中運行當前被丟棄的任務
- DiscardOledestPolicy策略:該策略將丟棄最老的一個請求,也就是即將被執行的第一個任務,並嘗試再次提交任務
- DiscardPolicy策略:該策略默默丟棄無法處理的任務,不予任何處理。
總結
首先先上一張圖,針對這張圖來進行總結:
- 主執行緒進行執行緒池的呼叫,執行緒池執行execute方法
- 執行緒池通過
addWorker
進行建立執行緒,並將執行緒放入到執行緒池中,這裡我們看到第二步是將執行緒新增到核心執行緒中,其實執行緒池內部不分核心執行緒和非核心執行緒,只是根據corePoolSize和maximumPoolSize設定的大小來進行區分,因為超過corePoolSize的執行緒會被回收,至於回收那些執行緒,是根據執行緒獲取任務的時候進行判斷,當前執行緒池數量大於corePoolSize,或者指定了allowCoreThreadTimeOut
為true,則他等待一定時間後會返回,不會一直等待 - 當執行緒池的數量達到corePoolSize時,執行緒池首先會將任務新增到佇列中
- 當佇列中任務也達到了佇列設定的最大值時,它會建立新的執行緒,注意的是此時的執行緒數量已經超過了corePoolSize,但是沒有達到maximumPoolSize最大值。
- 當執行緒池的執行緒數量達到了maximumPoolSize,則會相應拒絕策略。