1 前言
提起回滾,我們首先的能想到是事務回滾。這個詞對於一個有一年以上開發經驗不陌生。事務是一組組合成邏輯工作單元的操作,雖然系統中可能會出錯,但事務將控制和維護事務中每個操作的一致性和完整性。而對於目前SpringBoot盛行的當下,給一個service類添加事務也是輕而易舉的事。然而對於代碼層面的回滾,我們的回滾意識就很薄弱。那麼今天我們就通過JDK提供的併發包中的ThreadPoolExecutor看一下JDK框架中是怎麼處理事物的。
2 代碼片段
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 外層無限循環
for (;;) {
// 獲取線程池控制狀態
int c = ctl.get();
// 獲取狀態
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 初始的ctl爲RUNNING(-1),小於SHUTDOWN(0)
// 狀態大於等於SHUTDOWN,說明已經執行了shutdown()或者shutdownNow()方法,此時線程池進入結束狀態
if (rs >= SHUTDOWN &&
/*
* 狀態爲shutdown & 傳入的任務爲空 & 列隊不爲空的否命題,也就是這三個條件至少有一個不滿足
* 則返回false,也就是加入列隊失敗。挨個看一下每個爲false是什麼情況:
* 1.rs != SHUTDOWN ==> 由於上面判斷rs>=SHUTDOWN滿足,所以這裏是 rs>SHUTDOWN,也就是
* 對於狀態STOP/TIDYING/TERMINATED不能將任務加入列隊中。
* 2.要執行第二個判斷前提是rs == SHUTDOWN.當firstTask != null,可以直接判斷返回false.
* 3.工作列隊爲空,也直接返回false.
*/
! (rs == SHUTDOWN &&
// 第一個任務爲null
firstTask == null &&
// worker隊列不爲空
! workQueue.isEmpty()))
// 返回
return false;
/*
* 等走到這一步,包含幾種情況:
* 1.當前狀態處於RUNNING
* 2.當前狀態處於SHUTDOWN,firstTask== null 列隊不爲空向下執行
*/
for (;;) {
// worker數量
int wc = workerCountOf(c);
// worker數量大於等於最大容量
if (wc >= CAPACITY ||
// worker數量大於等於核心線程池大小或者最大線程池大小
//這裏決定是用核心線程大小比較還是最大的比較
//如果大於執行的線程數,加入工作列隊失敗。
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 比較並增加worker的數量
//當前工作線程數小於核心線程或者最大線程數量,通過CAS操作增加worker數量
if (compareAndIncrementWorkerCount(c))
// 跳出外層循環
break retry;
// 獲取線程池控制狀態
c = ctl.get(); // Re-read ctl
// 此次的狀態與上次獲取的狀態不相同
if (runStateOf(c) != rs)
// 跳過本循環體,繼續上層循環
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// worker開始標識
//主要用於判斷線程是否運行成功,如果不成功,需要將其移除列隊,並且將ctl-1。
boolean workerStarted = false;
// worker被添加標識
//這個標識主要用於判斷work是否填入列隊成功,如果成功則運行
boolean workerAdded = false;
Worker w = null;
try {
// 初始化worker
w = new Worker(firstTask);
// 獲取worker對應的線程
final Thread t = w.thread;
// 線程不爲null
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());
// 小於SHUTDOWN
if (rs < SHUTDOWN ||
// 等於SHUTDOWN並且firstTask爲null
(rs == SHUTDOWN && firstTask == null)) {
// 線程剛添加進來,還未啓動就存活
if (t.isAlive()) // precheck that t is startable
// 拋出線程狀態異常
throw new IllegalThreadStateException();
// 將worker添加到worker集合
workers.add(w);
// 獲取worker集合的大小
int s = workers.size();
// 隊列大小大於largestPoolSize
if (s > largestPoolSize) {
// 重新設置largestPoolSize
largestPoolSize = s;
}
// 設置worker已被添加標識
workerAdded = true;
}
} finally {
// 釋放鎖
mainLock.unlock();
}
// worker被添加
if (workerAdded) {
// 開始執行worker的run方法
t.start();
// 設置worker已開始標識
workerStarted = true;
}
}
} finally {
// worker沒有開始
if (! workerStarted)
// 添加worker失敗
addWorkerFailed(w);
}
return workerStarted;
}
這段代碼主要用於將線程添加到線程池中,通讀之後,我們發現這60多行(不包括註釋)代碼主要乾了三件事。
1) workCount變量+1,見代碼[48行] if (compareAndIncrementWorkerCount(c))
2)將線程封裝成Worker對象,加入workers變量中,見代碼 [94行] workers.add(w);
3)啓動線程。見代碼[112行]:t.start();
如果讓我們寫,我想也就是三四行代碼(最多再加個拋出異常什麼的)就能解決掉,但是JDK框架卻寫了將近60行代碼來保證代碼的健壯性及安全性。
我們今天主要是看回滾,所以其他代碼我們就不去深究。
2. 涉及到回滾的變量
boolean workerStarted = false;
boolean workerAdded = false;
workerStarted:主要記錄線程是否啓動成功。
workderAdded:主要記錄work是否添加到works變量中。
2.1 workerAdded設置
workerAdded設置是在代碼的94-103行,也就是將先前線程封裝的work成功放到works集合中的之後,修改此變量。當然如果沒有添加成功,這裏的標識還是不能被修改的。
2.2 workerStarted設置
workerAdded設置是在代碼的110-114行。這裏運行線程有個前提,就是將將先前線程封裝的work成功放到works集合中的之後,也就是workerAdded爲true的情況下。當線程運行成功,workerStarted會被設置成true。
2.3 代碼回滾
在這段代碼中我們發現有兩處處出現try語句,然而這兩處並沒有catch語句,而是在finnaly代碼塊自行處理了這些異常。處理的方式就是代碼回滾。
這裏有一點設計巧妙的地方是,如果【workers.add(w);】執行成功,但是workerAdded變量沒有設置成功,首先代碼110-114行不會被執行,workerStarted仍然是false,最終在119-122行執行代碼回滾。如果workerAdded設置成功,線程啓動失敗,即workerStarted = true;不會設置成功,最終finally也會進行代碼回滾。只有workerAdded和workerStarted都設置成功,纔不會進行代碼回滾。
2.4 代碼回滾的邏輯
當需要執行代碼回滾,必須先明確這個方法幹了那些事,上面提到總共做了三件事。那麼我們看看代碼回滾的邏輯實施對這三件事進行回滾。
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
通過上面代碼也看執行了works移除當前線程,workerCount-1的操縱。
3 回滾的場景
對於單個方法,如果我們在方法體中修改了成員變量,比如上面代碼中的workCount和ctl變量的情況下,如果在又任何異常拋出,我們根據業務場景將其設置還原。
最後還有一個小問題,就是上述代碼中在設置ctl過程中並沒有上鎖,而在works的add中卻上鎖了,大家可以想想其中的原因。