今天講一個牛逼而實用的概念,序列執行緒封閉
。物件池
是序列執行緒封閉的典型應用場景;執行緒池
糅合了物件池技術,但核心實現不依賴於物件池,很容易產生誤會。本文從序列執行緒封閉和物件池入手,最後通過原始碼分析執行緒池的核心原理,釐清物件池與執行緒池之間的誤會。
JDK版本:oracle java 1.8.0_102
執行緒封閉與序列執行緒封閉
執行緒封閉
執行緒封閉是一種常見的執行緒安全設計策略:僅在固定的一個執行緒內訪問物件,不對其他執行緒共享。
使用執行緒封閉技術,物件O始終只對一個執行緒T1可見,“單執行緒”中自然不存線上程安全的問題。
ThreadLocal是常用的執行緒安全工具,見原始碼|ThreadLocal的實現原理。執行緒封閉在Servlet及高層的web框架Spring等中應用不少。
序列執行緒封閉
執行緒封閉雖然好用,卻限制了物件的共享。序列執行緒封閉改進了這一點:物件O只能由單個執行緒T1擁有,但可以通過安全的釋出物件O來轉移O的所有權;在轉移所有權後,也只有另一個執行緒T2能獲得這個O的所有權,並且釋出O的T1不會再訪問O。
所謂“所有權”,指修改物件的權利。
相對於執行緒封閉,序列執行緒封閉使得任意時刻,最多僅有一個執行緒擁有物件的所有權。當然,這不是絕對的,只要執行緒T1事實不會再修改物件O,那麼就相當於僅有T2擁有物件的所有權。序列線層封閉讓物件變得可以共享(雖然只能序列的擁有所有權),靈活性得到大大提高;相對的,要共享物件就涉及安全釋出的問題,依靠BlockingQueue等同步工具很容易實現這一點。
物件池是序列執行緒封閉的經典應用場景,如資料庫連線池等。
物件池
物件池利用了序列封閉:將物件O“借給”一個請求執行緒T1,T1使用完再交還給物件池,並保證“未擅自發布該物件”且“以後不再使用”;物件池收回O後,等T2來借的時候再把它借給T2,完成物件所有權的傳遞。
猴子擼了一個簡化版的執行緒池,使用者只需要覆寫newObject()方法:
public abstract class AbstractObjectPool<T> {
protected final int min;
protected final int max;
protected final List<T> usings = new LinkedList<>();
protected final List<T> buffer = new LinkedList<>();
private volatile boolean inited = false;
public AbstractObjectPool(int min, int max) {
this.min = min;
this.max = max;
if (this.min < 0 || this.min > this.max) {
throw new IllegalArgumentException(String.format(
"need 0 <= min <= max <= Integer.MAX_VALUE, given min: %s, max: %s", this.min, this.max));
}
}
public void init() {
for (int i = 0; i < min; i++) {
buffer.add(newObject());
}
inited = true;
}
protected void checkInited() {
if (!inited) {
throw new IllegalStateException("not inited");
}
}
abstract protected T newObject();
public synchronized T getObject() {
checkInited();
if (usings.size() == max) {
return null;
}
if (buffer.size() == 0) {
T newObj = newObject();
usings.add(newObj);
return newObj;
}
T oldObj = buffer.remove(0);
usings.add(oldObj);
return oldObj;
}
public synchronized void freeObject(T obj) {
checkInited();
if (!usings.contains(obj)) {
throw new IllegalArgumentException(String.format("obj not in using queue: %s", obj));
}
usings.remove(usings.indexOf(obj));
buffer.add(obj);
}
}
複製程式碼
AbstractObjectPool具有以下特性:
- 支援設定最小、最大容量
- 物件一旦申請就不再釋放,避免了GC
雖然很簡單,但大可以用於一些時間敏感、資源充裕的場景。如果時間進一步敏感,可將getObject()、freeObject()改寫為併發程度更高的版本,但記得保證安全釋出安全回收;如果資源不那麼充裕,可以適當增加物件回收策略。
可以看到,一個物件池的基本行為包括:
- 建立物件newObject()
- 借取物件getObject()
- 歸還物件freeObject()
典型的物件池有各種連線池、常量池等,應用非常多,模型也大同小異,不做解析。令人迷惑的是執行緒池,很容易讓人誤以為執行緒池的核心原理也是物件池,下面來追一遍原始碼。
執行緒池
首先擺出結論:執行緒池糅合了物件池模型,但核心原理是生產者-消費者模型。
繼承結構如下:
使用者可以將Runnable(或Callables)例項提交給執行緒池,執行緒池會非同步執行該任務,返回響應的結果(完成/返回值)。
猴子最喜歡的是submit(Callable<T> task)
方法。我們從該方法入手,逐步深入函式棧,探究執行緒池的實現原理。
submit()
submit()方法在ExecutorService介面中定義,AbstractExecutorService實現,ThreadPoolExecutor直接繼承。
public abstract class AbstractExecutorService implements ExecutorService {
...
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
...
}
複製程式碼
AbstractExecutorService#newTaskFor()建立一個RunnableFuture型別的FutureTask。
核心是execute()方法。
execute()
execute()方法在Executor介面中定義,ThreadPoolExecutor實現。
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
...
}
複製程式碼
我們暫且忽略執行緒池的池化策略。關注一個最簡單的場景,看能不能先回答一個問題:執行緒池中的任務如何執行?
核心是addWorker()方法。以8行的引數為例,此時,執行緒池中的執行緒數未達到最小執行緒池大小corePoolSize,通常可以直接在9行返回。
addWorker()
簡化如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private boolean addWorker(Runnable firstTask, boolean core) {
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 {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN) {
workers.add(w);
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
...
}
複製程式碼
我去掉了很多用於管理執行緒池、維護執行緒安全的程式碼。假設執行緒池未關閉,worker(即w,下同)新增成功,則必然能夠將worker新增至workers中。workers是一個HashSet:
private final HashSet<Worker> workers = new HashSet<Worker>();
複製程式碼
哪裡是物件池?
如果說與物件池有關,那麼workers即相當於示例程式碼中的using,應用了物件池模型;只不過這裡的using是一直增長的,直到達到最大執行緒池大小maximumPoolSize。
但是很明顯,執行緒池並沒有將執行緒釋出出去,workers也僅僅完成using“儲存執行緒”的功能。那麼,執行緒池中的任務如何執行呢?跟執行緒池有沒有關係?
哪裡又不是?
注意9、17、24行:
- 9行將我們提交到執行緒池的firstTask封裝入一個worker。
- 17行將worker加入workers,維護起來
- 24行則啟動了worker中的執行緒t
核心在與這三行,但執行緒池並沒有直接在addWorker()中啟動任務firstTask,代之以啟動一個worker。最終任務必然被啟動,那麼我們繼續看Worker如何啟動這個任務。
Worker
Worker實現了Runnable介面:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
...
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
...
}
複製程式碼
為什麼要將構造Worker時的引數命名為firstTask?因為當且僅當需要建立新的Worker以執行任務task時,才會呼叫建構函式。因此,任務task對於新Worker而言,是第一個任務firstTask。
Worker的實現非常簡單:將自己作為Runable例項,構造時在內部建立並持有一個執行緒thread。Thread和Runable的使用大家很熟悉了,核心是Worker的run方法,它直接呼叫了runWorker()方法。
runWorker()
敲黑板!!!
重頭戲來了。簡化如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
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();
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);
}
}
...
}
複製程式碼
我們在前面將要執行的任務賦值給firstTask,5-6行首先取出任務task,並將firstTask置為null。因為接下來要執行task,firstTask欄位就沒有用了。
重點是10-31行的while迴圈。下面分情況討論。
case1:第一次進入迴圈,task不為null
case1對應前面作出的諸多假設。
第一次進入迴圈時,task==firstTask,不為null,使10行布林短路直接進入迴圈;從而16行執行的是firstTask的run()方法;異常處理不表;最後,finally程式碼塊中,task會被置為null,導致下一輪迴圈會進入case2。
case2:非第一次進入迴圈,task為null
case2是更普遍的情況,也就是執行緒池的核心。
case1中,task被置為了null,使10行布林表示式執行第二部分(task = getTask()) != null
(getTask()稍後再講,它返回一個使用者已提交的任務)。假設task得到了一個已提交的任務,從而16行執行的是新獲得的任務task的run()方法。後同case1,最後task仍然會被置為null,以後迴圈都將進入case2。
getTask()
任務從哪來呢?簡化如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
...
}
複製程式碼
我們先看最簡單的,19-28行。
首先,workQueue是一個執行緒安全的BlockingQueue,大部分時候使用的實現類是LinkedBlockingQueue,見原始碼|併發一枝花之BlockingQueue:
private final BlockingQueue<Runnable> workQueue;
複製程式碼
假設timed為false,則呼叫阻塞的take()方法,返回的r一定不是null,從而12行退出,將任務交給了某個worker執行緒。
一個小細節有點意思:前面每個worker執行緒runWorker()方法時,在迴圈中加鎖粒度在worker級別,直接使用的lock同步;但因為每一個woker都會呼叫getTask(),考慮到效能因素,原始碼中getTask()中使用樂觀的CAS+SPIN實現無鎖同步。關於樂觀鎖和CAS,可以參考我的另一篇文章原始碼|併發一枝花之ConcurrentLinkedQueue【偽】。
workQueue中的元素從哪來呢?這就要回顧execute()方法了。
execute()
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
...
}
複製程式碼
前面以8行的引數為例,此時,執行緒池中的執行緒數未達到最小執行緒池大小corePoolSize,通常可以直接在9行返回。進入8行的條件是“當前worker數小於最小執行緒池大小corePoolSize”
。
如果不滿足,會繼續執行到12行。isRunning(c)
判斷執行緒池是否未關閉,我們關注未關閉的情況;則會繼續執行布林表示式的第二部分workQueue.offer(command)
,嘗試將任務command放入佇列workQueue。
workQueue.offer()的行為取決於執行緒池持有的BlockingQueue例項。Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()建立的執行緒池使用LinkedBlockingQueue,而Executors.newCachedThreadPool()建立的執行緒池則使用SynchronousQueue。以LinkedBlockingQueue為例,建立時不配置容量,即建立為無界佇列,則LinkedBlockingQueue#offer()永遠返回true,從而進入12-18行。
更細節的內容不必關心了,當workQueue.offer()返回true時,已經將任務command放入了佇列workQueue。當未來的某個時刻,某worker執行完某一個任務之後,會從workQueue中再取出一個任務繼續執行,直到執行緒池關閉,直到海枯石爛。
CachedThreadPool是一種無界執行緒池,使用SynchronousQueue能進一步提升效能,簡化程式碼結構。留給讀者分析。
case2小結
可以看到,實際上,執行緒池的核心原理與物件池模型無關,而是生產者-消費者模型:
- 生產者(呼叫submit()或execute()方法)將任務task放入佇列
- 消費者(worker執行緒)迴圈從佇列中取出任務處理任務(執行task.run())。
鉤子方法
回到runWorker()方法,在執行任務的過程中,執行緒池保留了一些鉤子方法,如beforeExecute()、afterExecute()。使用者可以在實現自己的執行緒池時,可以通過覆寫鉤子方法為執行緒池新增功能。
但猴子不認為鉤子方法是一種好的設計。因為鉤子方法大多依賴於原始碼實現,那麼除非瞭解原始碼或API宣告絕對的嚴謹正確,否則很難正確使用鉤子方法。等發生錯誤時再去了解實現,可能就太晚了。說到底,還是不要使用類似extends這種表達“擴充套件”語義的語法來實現繼承,詳見Java中如何恰當的表達“繼承”與“擴充套件”的語義?。
當然,鉤子方法也是極其方便的。權衡看待。
總結
相對於執行緒封閉,序列執行緒封閉離使用者的距離更近一些,簡單靈活,實用性強,很容易掌握。而執行緒封閉更多淪為單純的設計策略,單純使用執行緒封閉的場景不多。
執行緒池與序列執行緒封閉、物件池的關係不大,但經常被混為一談;沒看過原始碼的很難想到其實現方案,面試時也能立分高下。
執行緒池的實現很有意思。在追原始碼之前,猴子一直以為執行緒池就是把執行緒存起來,用的時候取出來執行任務;看了原始碼才知道實現如此之妙,簡潔優雅效率高。原始碼才是最好的老師。
本文連結:原始碼|從序列執行緒封閉到物件池、執行緒池
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。