現在CPU都是有多個核心,并行已經成為事實,一方面我們希望最大限度利用機器性能(利用多線程提高吞吐率),另一方面機器的硬件資源是有限的,我們也不能無限制的去申請,幸運的是,JDK已經為我們提供了ExecutorService的實現,還提供了Executors工廠類方便我們生成模板線程池,但是簡單背后一定是復雜,這篇文章就是深入線程池的源碼去一探究竟。
開始之前,需要明確幾個概念,方便后面理解線程池的運行原理。
核心線程(corePool):線程池最終執行任務的角色肯定還是線程,同時我們也會限制線程的數量,所以我們可以這樣理解核心線程,有新任務提交時,首先檢查核心線程數,如果核心線程都在工作,而且數量也已經達到最大核心線程數,則不會繼續新建核心線程,而會將任務放入等待隊列。
等待隊列 (workQueue):等待隊列用于存儲當核心線程都在忙時,繼續新增的任務,核心線程在執行完當前任務后,也會去等待隊列拉取任務繼續執行,這個隊列一般是一個線程安全的阻塞隊列,它的容量也可以由開發者根據業務來定制。
非核心線程:當等待隊列滿了,如果當前線程數沒有超過最大線程數,則會新建線程執行任務,那么核心線程和非核心線程到底有什么區別呢?說出來你可能不信,本質上它們沒有什么區別,創建出來的線程也根本沒有標識去區分它們是核心還是非核心的,線程池只會去判斷已有的線程數(包括核心和非核心)去跟核心線程數和最大線程數比較,來決定下一步的策略。
線程活動保持時間 (keepAliveTime):線程空閑下來之后,保持存貨的持續時間,超過這個時間還沒有任務執行,該工作線程結束。
飽和策略 (RejectedExecutionHandler):當等待隊列已滿,線程數也達到最大線程數時,線程池會根據飽和策略來執行后續操作,默認的策略是拋棄要加入的任務。
一圖剩千言,上一張圖概括線程池的基本運作流程。
按我的習慣,先提出幾個問題,然后帶著問題去尋找答案。
- 線程池的線程是如何做到復用的。
- 線程池是如何做到高效并發的。
- 從線程池的設計中,我們能學到什么?
ThreadPoolExecutor
JDK中線程池的核心實現類是ThreadPoolExecutor,先看這個類的第一個成員變量ctl,AtomicInteger這個類可以通過CAS達到無鎖并發,效率比較高,這個變量有雙重身份,它的高三位表示線程池的狀態,低29位表示線程池中現有的線程數,這也是Doug Lea一個天才的設計,用最少的變量來減少鎖競爭,提高并發效率。
//CAS,無鎖并發
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//表示線程池線程數的bit數
private static final int COUNT_BITS = Integer.SIZE - 3;
//最大的線程數量,數量是完全夠用了
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
//1110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
private static final int RUNNING = -1 << COUNT_BITS;
//0000 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
private static final int SHUTDOWN = 0 << COUNT_BITS;
//0010 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
private static final int STOP = 1 << COUNT_BITS;
//0100 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
private static final int TIDYING = 2 << COUNT_BITS;
//0110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
//獲取線程池的狀態
private static int runStateOf(int c) { return c & ~CAPACITY; }
//獲取線程的數量
private static int workerCountOf(int c) { return c & CAPACITY; }
//組裝狀態和數量,成為ctl
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;
}
關于線程池的狀態,有5種,
- RUNNING, 運行狀態,值也是最小的,剛創建的線程池就是此狀態。
- SHUTDOWN,停工狀態,不再接收新任務,已經接收的會繼續執行
- STOP,停止狀態,不再接收新任務,已經接收正在執行的,也會中斷
- 清空狀態,所有任務都停止了,工作的線程也全部結束了
- TERMINATED,終止狀態,線程池已銷毀
它們的流轉關系如下:
execute/submit
向線程池提交任務有這2種方式,execute是ExecutorService接口定義的,submit有三種方法重載都在AbstractExecutorService中定義,都是將要執行的任務包裝為FutureTask來提交,使用者可以通過FutureTask來拿到任務的執行狀態和執行最終的結果,最終調用的都是execute方法,其實對于線程池來說,它并不關心你是哪種方式提交的,因為任務的狀態是由FutureTask自己維護的,對線程池透明。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
重點看execute的實現
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//第一步,獲取ctl
int c = ctl.get();
//檢查當前線程數是否達到核心線程數的限制,注意線程本身是不區分核心還是非核心,后面會進一步驗證
if (workerCountOf(c) < corePoolSize) {
//如果核心線程數未達到,會直接添加一個核心線程,也就是說在線程池剛啟動預熱階段,
//提交任務后,會優先啟動核心線程處理
if (addWorker(command, true))
return;
//如果添加任務失敗,刷新ctl,進入下一步
c = ctl.get();
}
//檢查線程池是否是運行狀態,然后將任務添加到等待隊列,注意offer是不會阻塞的
if (isRunning(c) && workQueue.offer(command)) {
//任務成功添加到等待隊列,再次刷新ctl
int recheck = ctl.get();
//如果線程池不是運行狀態,則將剛添加的任務從隊列移除并執行拒絕策略
if (! isRunning(recheck) && remove(command))
reject(command);
//判斷當前線程數量,如果線程數量為0,則添加一個非核心線程,并且不指定首次執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//添加非核心線程,指定首次執行任務,如果添加失敗,執行異常策略
else if (!addWorker(command, false))
reject(command);
}
/*
* addWorker方法申明
* @param core if true use corePoolSize as bound, else
* maximumPoolSize. (A boolean indicator is used here rather than a
* value to ensure reads of fresh values after checking other pool
* state).
* @return true if successful
*/
private boolean addWorker(Runnable firstTask, boolean core) {
//.....
}
這里有2個細節,可以深挖一下。
- 可以看到execute方法中沒有用到重量級鎖,ctl雖然可以保證本身變化的原子性,但是不能保證方法內部的代碼塊的原子性,是否會有并發問題?
- 上面提到過,addWorker方法可以添加工作線程(核心或者非核心),線程本身沒有核心或者非核心的標識,core參數只是用來確定 當前線程數的比較對象是線程池設置的核心線程數還是最大線程數,真實情況是不是這樣?
addWorker
添加線程的核心方法,直接看源碼
private boolean addWorker(Runnable firstTask, boolean core) {
//相當于goto,雖然不建議濫用,但這里使用又覺得沒一點問題
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//如果線程池的狀態到了SHUTDOWN或者之上的狀態時候,只有一種情況還需要繼續添加線程,
//那就是線程池已經SHUTDOWN,但是隊列中還有任務在排隊,而且不接受新任務(所以firstTask必須為null)
//這里還繼續添加線程的初衷是,加快執行等待隊列中的任務,盡快讓線程池關閉
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//傳入的core的參數,唯一用到的地方,如果線程數超過理論最大容量,如果core是true跟最大核心線程數比較,否則跟最大線程數比較
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通過CAS自旋,增加線程數+1,增加成功跳出雙層循環,繼續往下執行
if (compareAndIncrementWorkerCount(c))
break retry;
//檢測當前線程狀態如果發生了變化,則繼續回到retry,重新開始循環
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//走到這里,說明我們已經成功的將線程數+1了,但是真正的線程還沒有被添加
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//添加線程,Worker是繼承了AQS,實現了Runnable接口的包裝類
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());
//檢查線程狀態,還是跟之前一樣,只有當線程池處于RUNNING,或者處于SHUTDOWN并且firstTask==null的時候,這時候創建Worker來加速處理隊列中的任務
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//線程只能被start一次
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//workers是一個HashSet,添加我們新增的Worker
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//啟動Worker
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
分析完addWorker的源碼實現,我們可以回答上面留下的二個疑問,
- execute方法雖然沒有加鎖,但是在addWorker方法內部,加鎖了,這樣可以保證不會創建超過我們預期的線程數,大師在設計的時候,做到了在最小的范圍內加鎖,盡量減少鎖競爭,
- 可以看到,core參數,只是用來判斷當前線程數是否超量的時候跟corePoolSize還是maxPoolSize比較,Worker本身無核心或者非核心的概念。
繼續看Worker是怎么工作的
//Worker的run方法調用的是ThreadPoolExecutor的runWorker方法
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
//取出需要執行的任務,
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//如果task不是null,或者去隊列中取任務,注意這里會阻塞,后面會分析getTask方法
while (task != null || (task = getTask()) != null) {
//這個lock在這里是為了如果線程被中斷,那么會拋出InterruptedException,而退出循環,結束線程
w.lock();
//判斷線程是否需要中斷
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//任務開始執行前的hook方法
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 {
////任務開始執行后的hook方法
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
//Worker退出
processWorkerExit(w, completedAbruptly);
}
}
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.
//檢查線程池的狀態,如果已經是STOP及以上的狀態,或者已經SHUTDOWN,隊列也是空的時候,直接return null,并將Worker數量-1
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// 注意這里的allowCoreThreadTimeOut參數,字面意思是否允許核心線程超時,即如果我們設置為false,那么只有當線程數wc大于corePoolSize的時候才會超時
//更直接的意思就是,如果設置allowCoreThreadTimeOut為false,那么線程池在達到corePoolSize個工作線程之前,不會讓閑置的工作線程退出
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//確認超時,將Worker數-1,然后返回
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) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
現在我們可以回答文章一開始提出的三個問題中的前2個了
-
線程池的線程是如何做到復用的。
線程池中的線程在循環中嘗試取任務執行,這一步會被阻塞,如果設置了allowCoreThreadTimeOut為true,則線程池中的所有線程都會在keepAliveTime時間超時后還未取到任務而退出?;蛘呔€程池已經STOP,那么所有線程都會被中斷,然后退出。 -
線程池是如何做到高效并發的。
看整個線程池的工作流程,有以下幾個需要特別關注的并發點.
①: 線程池狀態和工作線程數量的變更。這個由一個AtomicInteger變量 ctl來解決原子性問題。
②: 向工作Worker容器workers中添加新的Worker的時候。這個線程池本身已經加鎖了。
③: 工作線程Worker從等待隊列中取任務的時候。這個由工作隊列本身來保證線程安全,比如LinkedBlockingQueue等。
用好Executors
JDK已經給我們提供了很方便的線程池工廠類Executors, 方便我們快速創建線程池,可能在閱讀源碼之前,我們在面對具體的業務場景時,到底該選擇哪種線程池配置是有疑問的,我們來看一下.
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool, 可以看到我們需要傳入一個線程數量的參數nThreads,這樣線程池的核心線程數和最大線程數都會設成nThreads, 而它的等待隊列是一個LinkedBlockingQueue,它的容量限制是Integer.MAX_VALUE, 可以認為是沒有邊界的。核心線程keepAlive時間0,allowCoreThreadTimeOut默認false。所以這個方法創建的線程池適合能估算出需要多少核心線程數量的場景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newSingleThreadExecutor, 有且只有一個線程在工作,適合任務順序執行,缺點但是不能充分利用CPU多核性能
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newCachedThreadPool, 核心線程數0,最大線程數Integer.MAX_VALUE, 線程keepAlive時間60s,用的隊列是SynchronousQueue,這種隊列本身不會存任務,只做轉發,所以newCachedThreadPool適合執行大量的,輕量級任務。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
newScheduledThreadPool, 執行周期性任務,類似定時器。
最后的問題:從線程池的設計中,我們能學到什么?
以我的個人體會,大概有四點
- 清楚實現原理,可以指導我們更好的使用。
- 在寫并發程序的時候,盡可能的縮小鎖的范圍,提高代碼的吞吐率。
- goto,不是一定不能用,而不是濫用,有些場景有奇效。
- 如果你需要多個線程安全的int型變量,考慮利用位運算把它們合并為一個。
全文完,水平有限,有疑問,歡迎交流!