一、線程池介紹
Java5開始,在util下提供了一個包,叫做JUC(java.util.concurrent),裏面提供了關於多線程、併發的一些工具包。例如鎖、多線程等工具都在這個包中。
我們知道一個線程的創建、銷燬過程是會消耗系統性能的,需要使用cpu,佔用內存,當頻繁大量的創建銷燬線程,這個消耗累積到會影響系統性能,爲了解決這一類問題,出現了“池化”的概念,“池化”意爲將資源放進一個池子內,需要的時候就從池中取,不用的時候就返還池中以便其它複用,實踐的例子很多,例如數據庫連接池、緩衝池以及本次講解的線程池等,中心思想就是複用這些連接,避免頻繁的創建銷燬,提高性能。
juc中的線程池的類關係如下圖:
Executor.class是頂層接口類,只定義了一個方法:execute()方法,入參爲Runnable接口;
ExecutorService.class是繼承Executor的接口,加入了一些方法submit()、shutdown()、shutdownNow()等方法的定義;
AbstractExecutorService抽象類實現了ExecutorService部分接口,比如submit()等方法;
ScheduleExecutorService接口繼承ExecutorService,顧名思義,增加了一些計劃執行函數,實現週期行執行任務的功能;
ThreadPoolExecutor類就是我們常用的線程池工具類了,它繼承於抽象類AbstractExecutorService,實現了線程池的任務、線程以及隊列等功能,接下來我們詳細介紹ThreadPoolExecutor的功能實現。
二、線程池源碼
如何使用線程池?假設我們有如下圖線程池實例代碼,可以看到大致創建線程池並且啓動的大概邏輯爲創建,執行:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ExecutorTest {
public static void main(String[] args) {
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running----");
}
}
//創建線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10), new ThreadPoolExecutor.DiscardPolicy());
//加入執行任務
executor.submit(new MyRunnable());
}
}
線程池構造參數含義?可以看到創建一個線程池,構造方法中需要傳入很多參數,每個參數都有很重要的含義,下表列出了每個參數的含義:
參數名稱 | 含義 | 備註 |
int corePoolSize |
核心線程 | 線程池核心線程數,包括空閒線程 |
int maximumPoolSize |
最大線程數 | 線程池所允許的最大線程數量 |
long keepAliveTime |
線程存活時間 | 線程空閒時間超過該值則銷燬(對核心線程起作用需配置參數) |
TimeUnit unit |
時間單位 | 線程空閒存活時間單位 |
BlockingQueue<Runnable> workQueue |
阻塞隊列 | 當核心線程用盡,將新任務加入隊列 |
ThreadFactory threadFactory |
線程工廠 | 創建線程的工廠 |
RejectedExecutionHandler handler |
拒絕策略 | 當隊列滿時,根據該策略處理新到達的任務 |
注:boolean allowCoreThreadTimeOut決定keepAliveTime是否對核心線程有作用;
ThreadPoolExecutor提供了四種拒絕策略:
- 1.AbortPolicy:默認策略,丟棄任務,並且拋出RejectedExecutionException;
- 2.DiscardPolicy:丟棄任務,什麼都不做;
- 3.DiscardOldestPolicy:丟棄老任務,執行該新任務;
- 4.CallerRunsPolicy:調用者線程執行該任務;
注:也可以自定義拒絕策略,實現RejectedExecutionHandler接口。
ThreadPoolExecutor提供四種基礎阻塞隊列:頂層是BlockingQueue接口
- 1.ArrayBlockingQueue:無界阻塞隊列,底層爲數組;
- 2.LinkedBlockingQueue:無界或有界阻塞隊列,底層爲鏈表;
- 3.SynchronousQueue:不存儲元素,一個線程執行元素插入,需等待另一個線程執行移除元素,否則阻塞插入操作;
- 4.PriorityBlockingQueue:無界的,帶有優先級的阻塞隊列;
注:隊列一般需要設置上限,需要注意無界隊列,如果過多的任務堆積於隊列中,有oom風險。
ThreadPoolExecutor線程池狀態流轉:
線程池中基本成員參數?在線程池中維護了一些參數,例如狀態、線程組以及鎖等信息,如下圖所示:
//32位,前3位代表線程池狀態,後29位代表線程池數量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//線程數量位32 - 3 = 29
private static final int COUNT_BITS = Integer.SIZE - 3;
//線程數容量000 11111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//各個運行狀態標誌(值:running<~<terminated)
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
//獲取ctl、線程池狀態、線程數
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
//阻塞隊列,任務容器
private final BlockingQueue<Runnable> workQueue;
//鎖,增加線程數、完成任務數等使用
private final ReentrantLock mainLock = new ReentrantLock();
//線程的容器
private final HashSet<Worker> workers = new HashSet<Worker>();
//鎖的條件功能
private final Condition termination = mainLock.newCondition();
//線程數量峯值
private int largestPoolSize;
//完成的任務數量
private long completedTaskCount;
//創建線程的工廠
private volatile ThreadFactory threadFactory;
//拒絕策略句柄
private volatile RejectedExecutionHandler handler;
//空閒線程存活時間
private volatile long keepAliveTime;
//是否主線程也有過期時間
private volatile boolean allowCoreThreadTimeOut;
//核心線程數量
private volatile int corePoolSize;
//最大線程數量
private volatile int maximumPoolSize;
線程池啓動過程?當我們創建一個線程池後,添加並執行任務時:
1.首先判斷當前線程的數量是否小於核心線程數量,如果小於,則直接創建核心線程並執行此任務,如果大於等於,則嘗試把此任務加入到阻塞隊列;
2.如果加入阻塞隊列成功,則等待空閒線程來阻塞隊列拉取任務並執行;
3.如果加入阻塞隊列失敗,則說明隊列已經滿了,這時候需要判斷當前線程數量是否小於最大線程數,如果小於,則創建非核心線程並執行該任務;
4.如果大於等於最大線程數,則使用拒絕策略處理該任務;
線程池的大概執行邏輯如上步驟,接下來我們看代碼。我們知道,線程池的入口執行方法是submit()以及execute(),前者實際上也是調用的後者,因此我們從execute()方法入口開始介紹執行過程:
public void execute(Runnable command) {
if (command == null)//傳入的任務爲null,直接拋出空指針異常
throw new NullPointerException();
int c = ctl.get();//獲取ctl,拿到線程池狀態、線程數量
//1.獲取線程數量,和核心線程數量比較
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))//小於核心線程,則創建核心線程,並且把該任務加入到該線程
return;
c = ctl.get();//創建核心線程失敗(可能線程池非運行狀態、核心線程剛滿),重新獲取ctl
}
//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);//超出最大線程數量或非運行狀態,拒絕策略處理
}
可以看出主要判斷邏輯爲:1.核心線程是否滿;2.隊列是否滿;3.是否超過最大線程數;
上述過程的流程圖如下圖所示:
上述過程中,一個重要的方法addWorker()是新建一個worker,並把任務放入該worker中執行,下面我們分析該方法的執行過程:
private boolean addWorker(Runnable firstTask, boolean core) {
//1.增加線程數量
retry://goto 標識
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))//如果線程池非運行狀態,且是停止狀態、任務爲空、隊列爲空,則返回false,添加失敗
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))//規則校驗線程數量
return false;
if (compareAndIncrementWorkerCount(c))//CAS增加線程數量
break retry;//增加線程數量成功,跳出外循環
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)//增加線程數量失敗,線程池狀態改變則開始外循環,狀態未變則開始內循環
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//2.新建worker,執行線程任務
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
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());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {//如果線程池是運行狀態或者是停止狀態且任務爲空,若此時線程狀態是活躍,則拋異常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;//更新線程數峯值
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
當在addWorker()方法中添加成功後,執行了start()函數,會去調用Worker類的run()方法,run()方法調用runWoker()方法,執行過程如下:
//Woker方法重寫的run()方法
public void run() {
runWorker(this);
}
//Worker的任務執行方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {//循環從隊列獲取任務
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
if ((runStateAtLeast(ctl.get(), STOP) ||
(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;
w.completedTasks++;//完成的任務數量自增
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
線程池如何停止?在上方線程池狀態流轉圖中可以看到,線程池可以通過shutdown()、shutdownNow()方法使線程池到達shutdown、stop停止狀態,在運二者區別在於對於隊列以及執行中的任務的處理方式,前者會等待隊列及正在運行的任務完成纔會執行退出邏輯,後者會終止正在運行的任務,剔除並返回所有隊列中的任務,是比較粗暴的,下面介紹兩種結束方式:
shutdownNow()方法執行邏輯?設置線程池狀態爲STOP,中斷所有未中斷線程,移除所有任務並返回,
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);//CAS設置狀態爲STOP
interruptWorkers();//中斷所有線程
tasks = drainQueue();//移除所有任務,並返回
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
中斷方法是比較粗暴的:
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
對於隊列中未執行的任務,直接拋棄並返回,那對於正在執行的任務是如何處理呢?文章中部圖runWoker()方法中可以看到循環獲取當前線程和阻塞隊列裏的線程,然後加鎖、執行、結束放鎖。運行中的線程改變了中斷標誌,如果處於阻塞狀態(IO阻塞)則會拋異常,然後結束本線程,如果是正常執行線程,則執行完畢後退出。
如果在getTask()方法返回null,則線程完畢,方法如下,可以看到開始會判斷線程池的狀態,shutdownNow()此時已經將下線程池標註爲STOP狀態了。
private Runnable getTask() {
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.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
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;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
shutdown()方法執行邏輯?相比於shutdownNow()方法,則更加優雅一些,代碼如下:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
中斷方法如下,可以看到有一個加鎖操作,而我們反過頭來看到,runWorker()方法,執行中的方法先獲取到鎖了,因此執行中的任務,這裏是無法終止的。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {//非中斷,並且嘗試加鎖成功
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
三、固定線程池
juc包中提供了一個類Executors,它提供了一些特殊固定功能的線程池,主要包括四種:
1.newSingleThreadPool:單一線程池,有且僅有一個線程;LinkedBlockingQueue是無界阻塞隊列,隊列可存放的大小爲Integer.MAX_VALUE
2.newFixedThreadPool:固定數量線程的線程池,LinkedBlockingQueue是無界阻塞隊列,隊列可存放的大小爲Integer.MAX_VALUE;
3.newCachedThreadPool:緩衝線程池,核心線程0,最大線程池數量是Integer.MAX_VALUE,空閒時間60s,使用SynchronousQueue,該隊列不存儲值;
4.newScheduledThreadPool:定時調度的線程池;
四、注意事項
1.合理配置線程數
線程數的多少直接關乎到線程池的性能效率以及對系統的影響,首先需要分析我們的業務是IO密集型還是CPU密集型,根據實際業務情況,配置合理的線程數。
IO密集型業務主要是阻塞進行IO操作,比較耗時,因此需要配置多的線程數,彌補阻塞的時間,一般配置爲CPU核數*2;
CPU密集型業務是進行大量的運算,多核的CPU可以提高計算速度,少量配置線程數,這樣增加多核計算機率,一般配置CPU核數+1的線程數量;
2.優雅關閉線程池
線程池中提供了兩種停止線程池的方法:shutdown()、shutdownNow(),兩者執行後的線程池狀態在上面圖中已經描述了,爲了保證任務合理的關閉,我們應該選取第一種方式關閉,停止接收外部任務,執行完阻塞隊列和正在執行的任務,然後,在通過方法awaitTermination()阻塞主線程,等待子線程任務完成後,優雅的關閉。
3.謹慎使用Executors提供的特殊線程池
Executors提供的幾種特殊線程池,雖然省去了傳入部分參數的麻煩,但是裏邊是有一部分的風險的。
單一或者固定數量的線程池,使用幾乎無界的阻塞隊列,如果任務執行很慢,但是任務很多,這個隊列會急劇增加,可能會佔用大量內存,並且因爲在使用無法剔除,導致oom。
緩衝線程池,隊列不會存儲任務,當任務量急劇增多,會瞬間創建大量的線程,這個也是很危險的。雖然我們前面已經根據業務評估使用不同的線程池,但是這些風險還是存在的,因此估計使用自定義的線程池。
五、資源地址
文檔:《Thinking in java》jdk1.8版本源碼