一、線程池優點
① 重用線程,提高性能(減少時間/空間開銷)
② 控制最大併發數(提高資源利用率、避免過多的資源競爭)
③ 管理線程(使線程的使用簡單、高效)
二、線程池的框架Executor
java中的線程池是通過Executor框架實現的,Executor 框架包括
線程池接口:Executor和ExecutorService
線程池類:ThreadPoolExecutor和Executors
任務類:Runnable和Callable
結果類:Future和FutureTask
Executor接口: 所有線程池的接口,只有一個方法
public interface Executor {
void execute(Runnable command);
}
ExecutorService接口: 基於Executor接口拓展了submit方法,允許傳入Callable對象
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
}
ThreadPoolExecutor:線程池的具體實現類,一般用的各種線程池都是基於這個類實現的
構造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize:線程池的核心線程數(是線程池的大小),默認情況下可以一直存活。可以通過設置allowCoreThreadTimeOut爲True,此時 核心線程數就是0,此時keepAliveTime控制所有線程的超時時間。
- maximumPoolSize:線程池允許的最大線程數,可以控制線程的最大併發數。個人認爲,它只是一種補救措施。當任務隊列滿了的情況下,如果還有新的任務來的話,會額外創建線程來執行該任務,所以需要通過maximumPoolSize來控制這種情況下的最大線程數量
- keepAliveTime: 指的是空閒線程結束的超時時間(問:這裏的空閒線程包括核心線程嗎?答:不包括,所以線程池最後的大小會保留爲corePoolSize數量)
- unit :是一個枚舉,表示 keepAliveTime 的單位
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
- workQueue:表示存放任務的BlockingQueue<Runnable>隊列(阻塞隊列已經加了顯示鎖Lock,因此支持併發的場景)
阻塞隊列一般有一下幾種選擇(這個參數的選擇也很重要,會對線程池的運行過程產生重大影響):
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用 LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
Executors: 主要提供了四個靜態方法來創建線程池,返回的線程池都實現了ExecutorService 接口
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
線程池的工作過程如下:
- 線程池剛創建時,裏面沒有一個線程。任務隊列是作爲參數傳進來的。
- 當調用 execute() 方法添加一個任務時,線程池會做如下判斷:
① 如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務;
② 如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列;
③ 如果這時候隊列滿了,而且正在運行的線程數量小於 maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務;
④ 如果隊列滿了,而且正在運行的線程數量大於或等於 maximumPoolSize,那麼線程池會拋出異常RejectExecutionException。 - 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
- 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運行的線程數大於 corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。
三、線程池實現原理
線程池的實現過程沒有用到Synchronized關鍵字,用的都是Volatile,Lock和同步(阻塞)隊列,Atomic相關類,FutureTask等等,因爲後者的性能更優。理解的過程可以很好的學習源碼中併發控制的思想。
1、線程複用過程
理解線程複用原理首先應瞭解線程生命週期。
在線程的生命週期中,它要經過新建、就緒、運行、阻塞和死亡5種狀態。
Thread通過new來新建一個線程,這個過程是是初始化一些線程信息,如線程名,id,線程所屬group等,可以認爲只是個普通的對象。調用Thread的start()後Java虛擬機會爲其創建方法調用棧和程序計數器,同時將hasBeenStarted爲true,之後調用start方法就會有異常。
處於這個狀態中的線程並沒有開始運行,只是表示該線程可以運行了。至於該線程何時開始運行,取決於JVM裏線程調度器的調度。當線程獲取cpu後,run()方法會被調用。不要自己去調用Thread的run()方法(否則就)。之後根據CPU的調度在就緒——運行——阻塞間切換,直到run()方法結束或其他方式停止線程,進入dead狀態。
所以實現線程複用的原理應該就是要保持線程處於存活狀態(就緒,運行或阻塞)。接下來來看下ThreadPoolExecutor是怎麼實現線程複用的。
在ThreadPoolExecutor主要Worker類來控制線程的複用。
private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null){
task.run();
}
}
Worker是一個Runnable,同時擁有一個thread,這個thread就是要開啓的線程,在新建Worker對象時新建一個Thread對象,同時將Worker自己作爲參數傳入Thread,這樣當Thread的start()方法調用時,運行的實際上是Worker的run()方法,接着到runWorker()中,有個while循環,一直從getTask()裏得到Runnable對象,順序執行。getTask()又是怎麼得到Runnable對象的呢?
依舊是簡化後的代碼:
private Runnable getTask() {
if(一些特殊情況) {
return null;
}
Runnable r = workQueue.take();
return r;
}
這個workQueue就是初始化ThreadPoolExecutor時存放任務的BlockingQueue隊列,這個隊列裏的存放的都是將要執行的Runnable任務。因爲BlockingQueue是個阻塞隊列,BlockingQueue.take()得到如果是空,則進入等待狀態直到BlockingQueue有新的對象被加入時喚醒阻塞的線程。所以一般情況Thread(這裏指的是線程池中的核心線程)的run()方法就不會結束,而是不斷執行從workQueue裏的Runnable任務,這就達到了線程複用的原理了。
2.控制最大併發數
① 那Runnable是什麼時候放入workQueue?
② Worker又是什麼時候創建?
③ Worker裏的Thread的又是什麼時候調用start()開啓新線程來執行Worker的run()方法的呢?
有上面的分析看出Worker裏的runWorker()執行任務時是一個接一個,串行進行的,那併發是怎麼體現的呢?
很容易想到是在execute(Runnable runnable)時會做上面的一些任務。看下execute裏是怎麼做的。
execute簡化後的代碼
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 當前線程數 < corePoolSize
if (workerCountOf(c) < corePoolSize) {
// 直接啓動新的線程。
if (addWorker(command, true))
return;
c = ctl.get();
}
// 活動線程數 >= corePoolSize
// runState爲RUNNING && 隊列未滿
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次檢驗是否爲RUNNING狀態
// 非RUNNING狀態 則從workQueue中移除任務並拒絕
if (!isRunning(recheck) && remove(command))
reject(command);// 採用線程池指定的策略拒絕任務
// 兩種情況:
// 1.非RUNNING狀態拒絕新的任務
// 2.隊列滿了啓動新的線程失敗(workCount > maximumPoolSize)
} else if (!addWorker(command, false))
reject(command);
}
addWorker簡化後的代碼
private boolean addWorker(Runnable firstTask, boolean core) {
int wc = workerCountOf(c);
if (wc >= (core ? corePoolSize : maximumPoolSize)) {
return false;
}
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();
}
根據代碼再來看上面提到的線程池工作過程中的添加任務的情況:
* 如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務;
* 如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列;
* 如果這時候隊列滿了,而且正在運行的線程數量小於 maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務;
* 如果隊列滿了,而且正在運行的線程數量大於或等於 maximumPoolSize,那麼線程池會拋出異常RejectExecutionException。
通過addWorker如果成功創建新的線程成功,則通過start()開啓新線程,同時將firstTask作爲這個Worker裏的run()中執行的第一個任務。
雖然每個Worker的任務是串行處理,但因爲共用一個workQueue,所以就會並行處理了。
所以根據corePoolSize和maximumPoolSize來控制最大併發數。大致過程可用下圖表示。
3、管理線程
通過線程池可以很好的管理線程的複用,控制併發數,以及銷燬等過程,線程的複用和控制併發上面已經講了,而線程的管理過程已經穿插在其中了,也很好理解。
在ThreadPoolExecutor有個ctl的AtomicInteger變量。通過這一個變量保存了兩個內容
- 所有線程的數量
- 每個線程所處的狀態
其中低29位存線程數,高3位存runState,通過位運算來得到不同的值
線程池有五種狀態來控制任務添加與執行。主要介紹以下三種:
- RUNNING狀態:線程池正常運行,可以接受新的任務並處理隊列中的任務;
- SHUTDOWN狀態:不再接受新的任務,但是會執行隊列中的任務;
- STOP狀態:不再接受新任務,不處理隊列中的任務
shutdown這個方法會將runState置爲SHUTDOWN,會終止所有空閒的線程,而仍在工作的線程不受影響,所以隊列中的任務仍然會被執行。shutdownNow方法將runState置爲STOP。和shutdown方法的區別,這個方法會終止所有的線程,所以隊列中的任務也不會被執行了。
四、深入剖析線程池及其原理
1、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類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作(這一點和HashMap是一樣,HashMap無論你用哪個構造器,最終都會去調用第三個構造函數)。
下面解釋下一下構造器中各個參數的含義:
- threadFactory:線程工廠,主要用來創建線程;
- handler:表示當拒絕處理任務時的策略,有以下四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
幾個線程池類的關係
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()是用來關閉線程池的
2、深入剖析線程池實現原理
這裏按照這樣的順序來介紹實現原理
- 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; //用來記錄已經執行完畢的任務個數
下面我們進入正題,看一下任務從提交到最終執行完畢經歷了哪些過程。
在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)
如果執行addIfUnderMaximumPoolSize方法失敗(返回true),則執行reject()方法進行任務拒絕處理。
如果執行完addIfUnderCorePoolSize這個方法返回false,然後接着判斷:
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,如果傳進去的參數爲null,則最後執行線程會阻塞在getTask方法直到阻塞隊列裏面有新的任務
4.任務緩存隊列及排隊策略
在前面我們多次提到了任務緩存隊列,即workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue,通常可以取下面三種類型:
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進行線程賦值,還可能立即創建新的線程來執行任務。
五、總結
此文章參考了大佬的博客,在此致敬一下大佬。(偷偷吐槽一下,大佬雖然講得很詳細,但是排版……)所參考的博客地址爲:https://blog.csdn.net/he90227/article/details/52576452#t2