寫在前面的話
并發(fā)編程里面,線程池這個一直就想寫一篇文章來總結下,但是直到并發(fā)編程系列的第12篇才寫的原因是線程池里面用到了AQS同步隊列和阻塞隊列等一些知識,所以為了鋪墊,就先把前面的知識點寫完了,到現(xiàn)在,終于可以總結一下線程池的實現(xiàn)原理了。
什么是線程池
在Java中,創(chuàng)建一個線程可以通過繼承Thread或者實現(xiàn)Runnable接口來實現(xiàn),但是,如果每個請求都創(chuàng)建一個新線程,那么創(chuàng)建和銷毀線程花費的時間和消耗的系統(tǒng)資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。
為了解決這個問題,就有了線程池的概念,線程池的核心邏輯是提前創(chuàng)建好若干個線程放在一個容器中。如果有任務需要處理,則將任務直接分配給線程池中的線程來執(zhí)行就行,任務處理完以后這個線程不會被銷毀,而是等待后續(xù)分配任務。同時通過線程池來重復管理線程還可以避免創(chuàng)建大量線程增加開銷。
創(chuàng)建線程池
為了方便使用,Java中的Executors類里面提供了幾個線程池的工廠方法,可以直接利用提供的方法創(chuàng)建不同類型的線程池:
- newFixedThreadPool:創(chuàng)建一個固定線程數(shù)的線程池
- newSingleThreadExecutor:創(chuàng)建只有1個線程的線程池
- newCachedThreadPool:返回一個可根據(jù)實際情況調整線程個數(shù)的線程池,不限制最大線程 數(shù)量,若用空閑的線程則執(zhí)行任務,若無任務則不創(chuàng)建線程。并且每一個空閑線程會在60秒 后自動回收。
- newScheduledThreadPool: 創(chuàng)建一個可以指定線程的數(shù)量的線程池,但是這個線程池還帶有 延遲和周期性執(zhí)行任務的功能,類似定時器。
FixedThreadPool
創(chuàng)建一個固定數(shù)量N個線程在一個共享的無邊界隊列上操作的線程池。在任何時候,最多N個線程被激活處理任務。如果所有線程都在活動狀態(tài)時又有新的任務被提交,那么新提交的任務會加入隊列等待直到有線程可用為止。
如果有任何線程在shutdown前因為失敗而被終止,那么當有新的任務需要執(zhí)行時會產(chǎn)生一個新的線程,新的線程將會一直存在線程池中,直到被顯式的shutdown。
示例
package com.zwx.concurrent.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
//FixedThreadPool - 固定線程數(shù)
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i=0;i<10;i++){
fixedThreadPool.execute(()-> {
System.out.println("線程名:" + Thread.currentThread().getName());
});
}
fixedThreadPool.shutdown();
}
}
輸出結果為:
可以看到,最多只有3個線程在循環(huán)執(zhí)行任務(運行結果是不一定的,但是最多只會有3個線程)。
FixedThreadPool調用了如下方法構造線程池:
SingleThreadExecutor
只有一個工作線程的執(zhí)行器。如果這個線程在正常關閉前因為執(zhí)行失敗而被關閉,那么就會重新創(chuàng)建一個新的線程加入執(zhí)行器。
這種執(zhí)行器可以保證所有的任務按順序執(zhí)行,并且在任何給定的時間內,確保活動的任務只有1個。
示例
package com.zwx.concurrent.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i=0;i<9;i++){
singleThreadExecutor.execute(()-> {
System.out.println("線程名:" + Thread.currentThread().getName());
});
}
}
}
singleThreadExecutor.shutdown();
運行結果只有1個線程:
SingleThreadExecutor調用了如下方法構造線程池:
CachedThreadPool
一個在需要處理任務時才會創(chuàng)建線程的線程池,如果一個線程處理完任務了還沒有被回收,那么線程可以被重復使用。
當我們調用execute方法時,如果之前創(chuàng)建的線程有空閑可用的,則會復用之前創(chuàng)建好的線程,否則就會創(chuàng)建新的線程加入到線程池中。
創(chuàng)建好的線程如果在60s內沒被使用,那么線程就會被終止并移出緩存。因此,這種線程池可以保持長時間空閑狀態(tài)而不會消耗任何資源。
示例
package com.zwx.concurrent.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i=0;i<9;i++){
cachedThreadPool.execute(()-> {
System.out.println("線程名:" + Thread.currentThread().getName());
});
}
cachedThreadPool.shutdown();
}
輸出結果可以看到,創(chuàng)建了9個不同的線程:
接下來我們對上面的示例改造一下,在執(zhí)行execute之前休眠一段時間:
package com.zwx.concurrent.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i=0;i<9;i++){
try {
Thread.sleep(i * 10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(()-> {
System.out.println("線程名:" + Thread.currentThread().getName());
});
}
cachedThreadPool.shutdown();
}
這時候輸出的結果就只有1個線程了,因為有部分線程可以被復用:
注意:這兩個示例的結果都不是固定的,第一種有可能也不會創(chuàng)建9個線程,第二種也有可能不止創(chuàng)建1個線程,具體要看線程的執(zhí)行情況。
CachedThreadPool調用了如下方法構造線程池
ScheduledThreadPool
創(chuàng)建一個線程池,它可以在調度命令給定的延遲后運行或定期執(zhí)行。這個相比較于其他的線程池,其自定義了一個子類ScheduledExecutorService繼承了ExecutorService。
示例
package com.zwx.concurrent.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i=0;i<9;i++){
scheduledThreadPool.execute(()->{
System.out.println("線程名:" + Thread.currentThread().getName());
});
}
scheduledThreadPool.shutdown();
}
}
輸出結果(執(zhí)行結果具有隨機性,最多只有3個線程執(zhí)行):
ScheduledThreadPool最終調用了如下方法構造線程池
線程池原理
根據(jù)上面的截圖可以看到,列舉的4中常用的線程池在構造時,最終調用的方法都是ThreadPoolExecutor類的構造方法,所以要分析原理,我們就去看看ThreadPoolExecutor吧!
構造線程池7大參數(shù)
下面就是ThreadPoolExecutor類中最完整的一個構造方法:
這個就是是構造線程池的核心方法,總共有7個參數(shù):
- corePoolSize:核心線程數(shù)量。一直保留在池中的線程,核心線程即使空閑狀態(tài)也不會被回收,除非設置了allowCoreThreadTimeOut屬性
- maximumPoolSize:最大線程數(shù)量。線程池中允許的最大線程數(shù),大于等于核心線程數(shù)
- keepAliveTime:活躍時間。當最大線程數(shù)比核心線程數(shù)更大時,超出核心的線程數(shù)的其他線程如果空間時間超過keepAliveTime會被回收
- TimeUnit:活躍時間的單位
- BlockingQueue:阻塞隊列。用于存儲尚等待被執(zhí)行的任務。
- ThreadFactory:創(chuàng)建線程的工廠類
- RejectedExecutionHandler:拒絕策略。當達到了線程邊界和隊列容量時提交的任務被阻塞時執(zhí)行的策略。
線程池執(zhí)行流程
execute(Runnable) 方法的主流程非常清晰:
根據(jù)上面源碼,可以得出線程池執(zhí)行流程圖如下:
源碼分析
首先看看ThreadPoolExecutor類中的ctl,是一個32位的int類型,其中將高3位用來表示線程數(shù)量,低29位用來表示,其中的計算方式都是采用二進制來計算。
其中各種狀態(tài)的轉換關系如下圖:
其中狀態(tài)的大小關系為:
RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED
addWork方法
private boolean addWorker(Runnable firstTask, boolean core) {
//第一段邏輯:線程數(shù)+1
retry:
for (;;) {
int c = ctl.get();//獲取線程池容量
int rs = runStateOf(c);//獲取狀態(tài)
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&//即:SHUTDOWN,STOP,TIDYING,TERMINATED
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))//即:rs==RUNNING,firstTask!=null,queue==null
return false;//如果已經(jīng)關閉,不接受任務;如果正在運行,且queue為null,也返回false
for (;;) {
int wc = workerCountOf(c);//獲取當前的工作線程數(shù)
//如果工作線程數(shù)大于等于容量或者大于等于核心線程數(shù)(最大線程數(shù)),那么就不能再添加worker
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))//cas增加線程數(shù),失敗則再次自旋嘗試
break retry;
c = ctl.get(); // Re-read ctl //再次獲取工作線程數(shù)
if (runStateOf(c) != rs)//不相等說明線程池的狀態(tài)發(fā)生了變化,繼續(xù)自旋嘗試
continue retry;
}
}
//第二段邏輯:將線程構造成Worker對象,并添加到線程池
boolean workerStarted = false;//工作線程是否啟動成功
boolean workerAdded = false;//工作線程是否添加成功
Worker w = null;
try {
w = new Worker(firstTask);//構建一個worker
final Thread t = w.thread;//去除worker中的線程
if (t != null) {
final ReentrantLock mainLock = this.mainLock;//獲取重入鎖
mainLock.lock();//上鎖
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());//獲得鎖之后,再次檢查狀態(tài)
//只有當前線程池是正在運行狀態(tài),[或是 SHUTDOWN 且 firstTask 為空],才能添加到 workers 集合中
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);//將新創(chuàng)建的 Worker 添加到 workers 集合中
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;//更新線程池中線程的數(shù)量
workerAdded = true;//添加線程(worker)成功
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();//這里就會去執(zhí)行Worker中的run()方法
workerStarted = true;//啟動成功
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);//如果啟動線程失敗,需要回滾
}
return workerStarted;
}
這個方法主要就是做兩件事:
- 一、將線程數(shù)+1
- 二、將線程構造成Worker對象,加入到線程池中,并調用start()方法啟動線程
Worker對象
上面這個方法繼承了AbstractQueuedSynchronizer,前面我們講述AQS同步隊列的時候知道,AQS就是一個同步器,那么既然有線程的同步器,這里為什么不直接使用,反而要繼承之后重寫呢?
這是因為AQS同步器內是支持鎖重入的,但是線程池這里的設計思想是并不希望支持重入,所以才會重寫一個AQS來避免重入。
Worker中state初始化狀態(tài)設置為-1,原因是在初始化Worker對象的時候,在線程真正執(zhí)行runWorker()方法之前,不能被中斷。而一旦線程構造完畢并開始執(zhí)行任務的時候,是允許被中斷的,所以在線程進入runWorker()之后的第一件事就是將state設置為0(無鎖狀態(tài)),也就是允許被中斷。
我們再看看Worker的構造器:
addWork方法執(zhí)行到這句:w = new Worker(firstTask);//構建一個worker 的時候,就會調用構造器創(chuàng)建一個Worker對象,state=-1,并且將當前任務作為firstTask,后面再運行的時候會優(yōu)先執(zhí)行firstTask。
上面addWorker方法在worker構造成功之后,就會調用worker.start方法,這時候就會去執(zhí)行Worker中的run()方法,這也是一種委派的方式
run()方法中調用了runWorker(this)方法,這個方法就是真正執(zhí)行任務的方法:
runWorker(this)方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
/**
* 表示當前worker線程允許中斷,因為new Worker默認的 state=-1,此處是調用
* Worker類的 tryRelease()方法,state置為 0,
* 而 interruptIfStarted()中只有 state>=0 才允許調用中斷
*/
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
/**
* 加鎖,這里加鎖不僅僅是為了防止并發(fā),更是為了當調用shutDown()方法的時候線程不被中斷,
* 因為shutDown()的時候在中斷線程之前會調用tryLock方法嘗試獲取鎖,獲取鎖成功才會中斷
*/
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
/**
* 如果是以下兩種情況,需要中斷線程
* 1.如果state>=STOP,且線程中斷標記為false
* 2.如果state<STOP,獲取中斷標記并復位,如果線程被中斷,那么,再次判斷state是否STOP
* 如果是的話,且線程中斷標記為false
*/
if ((runStateAtLeast(ctl.get(), STOP) ||//狀態(tài)>=STOP
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();//中斷線程
try {
beforeExecute(wt, task);//空方法,我們可以重寫它,在執(zhí)行任務前做點事情,常用于線程池運行的監(jiān)控和統(tǒng)計
Throwable thrown = null;
try {
task.run();//正式調用run()執(zhí)行任務
} 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);//執(zhí)行任務之后調用,也是個空方法,我們可以重寫它,在執(zhí)行任務后做點事情,常用于線程池運行的監(jiān)控和統(tǒng)計
}
} finally {
task = null;//將任務設置為空,那么下次循環(huán)就會通過getTask()方法從workerQueue中取任務了
w.completedTasks++;//任務完成數(shù)+1
w.unlock();
}
}
completedAbruptly = false;
} finally {
//核心線程會阻塞在getTask()方法中等待線程,除非設置了允許核心線程被銷毀,
// 否則正常的情況下只有非核心線程才會執(zhí)行這里
processWorkerExit(w, completedAbruptly);//銷毀線程
}
}
主要執(zhí)行步驟為:
- 1、首先釋放鎖,因為進入這個方法之后線程允許被中斷
- 2、首先看看傳入的firstTask是否為空,不為空則優(yōu)先執(zhí)行
- 3、如果firstTask為空(執(zhí)行完了),則嘗試從getTask()中獲取任務,getTask()就是從隊列l(wèi)里面獲取任務
- 4、如果獲取到任務則開始執(zhí)行,執(zhí)行的時候需要重新上鎖,因為執(zhí)行任務期間也不允許中斷
- 5、任務運行前后分別有一個空方法,我們可以在有需要的時候重寫這兩個方法,實現(xiàn)付線程池的監(jiān)控
- 6、如果獲取不到任務,則會執(zhí)行processWorkerExit方法銷毀線程
getTask()方法
private Runnable getTask() {
//上一次獲取任務是否超時,第一次進來默認false,第一次自旋后如果超時就會設置為true,則第二次自旋就會返回null
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
/**
* 1\. 線程池狀態(tài)為shutdown,那么就必須要等到workQueue為空才行,因為shutdown()狀態(tài)是需要執(zhí)行隊列中剩余任務的
* 2.線程池狀態(tài)為stop,那么就不需要關注workQueue中是否有任務
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();//線程池中的線程數(shù)-1
return null;//返回null的話,那么runWorker方法中就會跳出循環(huán),執(zhí)行finally中的processWorkerExit方法銷毀線程
}
int wc = workerCountOf(c);
// Are workers subject to culling?
//1.allowCoreThreadTimeOut-默認false,表示核心線程數(shù)不會超時
//2.如果總線程數(shù)大于核心線程數(shù),那就說明需要有線程被銷毀
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/**
* 1\. 線程數(shù)量超過maximumPoolSize可能是線程池在運行時被調用了setMaximumPoolSize()
* 被改變了大小,否則已經(jīng) addWorker()成功的話是不會超過maximumPoolSize。
* 2.timed && timedOut 如果為 true,表示當前操作需要進行超時控制,并且上次從阻塞隊列中
* 獲取任務發(fā)生了超時.其實就是體現(xiàn)了空閑線程的存活時間
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ://等待指定時間后返回
workQueue.take();//拿不到任務會一直阻塞(如核心線程)
if (r != null)
return r;//如果拿到任務了,返回給worker進行處理
timedOut = true;//走到這里就說明到了超期時間還沒拿到任務,設置為true,第二次自旋就可以直接返回null
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
這個方法主要步驟為:
- 1、首先判斷狀態(tài)是不是對的,如果是SHUTDOWN之類不符合要求的狀態(tài),那就直接返回null,并把線程數(shù)-1,而返回null之后前面的方法就會跳出while循環(huán),執(zhí)行銷毀線程流程。
- 2、判斷下是不是有設置超時時間或者最大線程數(shù)超過了核心線程數(shù)
- 3、根據(jù)上面的判斷決定是執(zhí)行帶有超時時間的poll方法還是take方法從隊列中獲取元素。
情況一:如果是執(zhí)行帶超時時間的poll方法,那么時間到了如果還沒取到元素,那么就返回空,這種情況說明當前系統(tǒng)并不繁忙,所以返回null之后線程就會被銷毀;
情況二:如果是執(zhí)行take方法,根據(jù)第2點的判斷知道,除非我們人為設置了核心線程可以被回收,否則核心線程就是會執(zhí)行take方法,如果獲取不到任務就會一直阻塞等待獲取到任務為止。
processWorkerExit方法
這是銷毀線程的方法,上面的getTask()方法返回空,就會執(zhí)行線程銷毀方法,因為getTask()當中已經(jīng)把線程數(shù)-1了,所以這里可以直接執(zhí)行線程銷毀工作。
直接調用的是workers集合的remove()方法,后面還有就是嘗試中止和一些異常異常情況的補償操作。
拒絕策略
JDK默認提供的拒絕策略有如下幾種:
- AbortPolicy:直接拋出異常,默認策略
- CallerRunsPolicy:用調用者所在的線程來執(zhí)行任務
- DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,并執(zhí)行當前任務
- DiscardPolicy:直接丟棄任務
我們也可以自定義自己的拒絕策略,只要實現(xiàn)RejectedExecutionHandler接口,重寫其中的唯一一個方法rejectedExecution就可以了。
常見的面試問題
線程池這一塊面試非常喜歡問,我們來舉幾個常見的問題:
問題一
Q:為什么不建議直接使用Executors來構建線程池?
A:用Executors 使得我們不用關心線程池的參數(shù)含義,這樣可能會導致問題,比如我們用newFixdThreadPool或者newSingleThreadPool.允許的隊列長度為Integer.MAX_VALUE,如果使用不當會導致大量請求堆積到隊列中導致OOM的風險而newCachedThreadPool,允許創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,也可能會導致大量 線程的創(chuàng)建出現(xiàn)CPU使用過高或者OOM的問題。而如果我們通過ThreadPoolExecutor來構造線程池的話,我們勢必要了解線程池構造中每個 參數(shù)的具體含義,會更加謹慎。
問題二
Q:如何合理配置線程池的大小?
A:要想合理地配置線程池,就必須首先分析任務特性,可以從以下幾個角度來分析:
- 任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
- 任務的優(yōu)先級:高、中和低。
- 任務的執(zhí)行時間:長、中和短。
- 任務的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接。
CPU密集型:
CPU密集型的特點是響應時間很快,cpu一直在運行,這種任務cpu 的利用率很高,那么線程數(shù)的配置應該根據(jù)CPU核心數(shù)來決定,CPU核心數(shù)=最大同時執(zhí)行線程數(shù),假如CPU核心數(shù)為4,那么服務器最多能同時執(zhí)行4個線程。過多的線程會導致上 下文切換反而使得效率降低。那線程池的最大線程數(shù)可以配置為cpu核心數(shù)+1。
IO密集型:
主要是進行IO操作,執(zhí)行IO操作的時間較長,這是cpu會處于空閑狀態(tài), 導致cpu的利用率不高,這種情況下可以增加線程池的大小。可以結合線程的等待時長來做判斷,等待時間越高,那么線程數(shù)也相對越多。一般可以配置cpu核心數(shù)的2倍。
一個公式:線程池設定最佳線程數(shù)目 = ((線程池設定的線程等待時間+線程 CPU 時間)/ 線程CPU時間 )* CPU數(shù)目
附:獲取CPU個數(shù)方法:Runtime.getRuntime().availableProcessors()
問題三
Q:線程池中的核心線程什么時候會初始化?
A:默認情況下,創(chuàng)建線程池之后,線程池中是沒有線程的,需要提交任務之后才會創(chuàng)建線程。 在實際中如果需要線程池創(chuàng)建之后立即創(chuàng)建線程,可以通過如下兩個方法:
- prestartCoreThread():初始化一個核心線程。
- prestartAllCoreThreads():初始化所有核心線程
問題四
Q:線程池被關閉時,如果還有任務在執(zhí)行,怎么辦?
A:線程池的關閉有兩個方法:
- shutdown()
不會立即終止線程池,要等所有任務緩存隊列中的任務都執(zhí)行完后才終止,但是不會接受新的任務 - shutdownNow()
立即終止線程池,并嘗試打斷正在執(zhí)行的任務,并且清空任務緩存隊列,返回尚未執(zhí)行的任務任務
問題五
Q:線程池容量是否可以動態(tài)調整?
A:可以通過兩個方法動態(tài)調整線程池的大小。
- setCorePoolSize():設置最大核心線程數(shù)
- setMaximumPoolSize():設置最大工作線程數(shù)
總結
本文從線程池的常見的四種用法使用示例開始入手,最終發(fā)現(xiàn)都調用了同一個類去構造線程池(ThreadPoolExecutor),所以我們就從ThreadPoolExecutor構造器開始分析了構建一個線程池的7大參數(shù),并從execute()方法開始逐步分析了線程池的使用原理,當然,其實線程池還有一個方法submit()也可以作為入口,這個會放在下篇并發(fā)系列講述Future/Callable的時候再去分析。