面試官:說下你對AQS的理解!
在技術面試的時候,“說下你對 AQS 的理解”,這個問題出現的概率屬實不低,而一些技術底子一般的同學,又非常容易被它復雜的底層源碼弄得暈頭轉向。
今天這篇文章,我們就以做減法的方式,將這個知識點徹底地大家講清楚。
AQS,是 AbstractQueuedSynchronizer(抽象隊列同步器)這個類的簡稱,也是 Java JUC 包中的靈魂,ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 都是通過其實現鎖或同步器的。
其核心思想為,在多線程并發訪問共享資源時,通過雙向鏈表實現的先進先出 CLH 隊列進行線程排隊,并通過由 volatile 修飾的 state 變量來標識資源的鎖占用狀態。
如下圖所示:
圖片
在 AQS 中提供了兩種資源獲取方式:
獨占模式(Exclusive),在同一時刻只能有一個線程獲取競態資源,比如:ReentrantLock。
共享模式(Share),在同一時刻可以有多個(參數設定)線程獲取競態資源,比如:CountDownLatch、Semaphore。
AQS 方法詳述
AQS 的方法大致分為三類,分別是獨占模式下的方法、共享模式下的方法、通過模板方法模式留給子類實現的方法。
獨占模式:
// 獲取鎖
public final void acquire(int arg)
// 以可中斷的方式獲取鎖
public final void acquireInterruptibly(int arg)
// 以帶超時時間的方式,嘗試獲取鎖
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 釋放鎖
public final boolean release(int arg)
共享模式:
// 獲取鎖
public final void acquireShared(int arg)
// 以可中斷的方式獲取鎖
public final void acquireSharedInterruptibly(int arg)
// 以帶超時時間的方式,嘗試獲取鎖
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 釋放鎖
public final boolean releaseShared(int arg)
需要子類實現的方法:
// 嘗試獲取獨占鎖
protected boolean tryAcquire(int arg);
// 嘗試釋放獨占鎖
protected boolean tryRelease(int arg);
// 嘗試獲取共享鎖
protected int tryAcquireShared(int arg);
// 嘗試釋放共享鎖
protected boolean tryReleaseShared(int arg);
// 判斷當前線程是否正在持有鎖
protected boolean isHeldExclusively();
看到 AQS 父類實現了一部分方法,也預留了一些方法讓 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等子類實現,我們想到了哪種設計模式?
是的,模板方法模式。
模板方法模式:定義一個操作中算法的框架,而將一些步驟延遲到子類中,模板方法使得子類可以不改變一個算法的結構,即可重定義該算法的某些步驟。
使用模板方法模式,可以將一個操作的復雜流程的實現步驟進行拆分,封裝在一系列基本方法中,在抽象父類提供一個模板方法來定義整體執行步驟,而通過其子類來覆蓋某個步驟,從而使得相同的執行步驟可以有不同的執行結果。
類結構如下:
圖片
模板方法模式的優點在于:
- 代碼復用性高,父類的模板方法和具體方法都可以供多個子類復用。
- 代碼靈活性高,可根據業務迭代情況,靈活選擇哪部分復用父類具體方法,哪部分進行子類覆蓋實現。
嗯,這些底層源碼的設計還是非常巧妙的,而設計模式本身也并不是有些人口中的過度設計的“花架子”。
ReentrantLock 與 AQS
接下來我們看下,ReentrantLock 是如何通過 AQS 來實現鎖機制的。
兩者間的 UML 圖如下所示:
圖片
從圖中可以看到,ReentrantLock 中有一個 Sync 內部類,Sync 繼承自 AQS,且 Sync 有兩個子類 FairSync 和 NonfairSync,分別用于實現公平鎖和非公平鎖。
我們梳理一下源碼,看看 ReentrantLock 如何實現非公平鎖的。
代碼入口如下,我們看只有兩個方法加上一個判斷。
public class ReentrantLock implements Lock, java.io.Serializable{
abstract static class Sync extends AbstractQueuedSynchronizer {
final void lock() {
if (!initialTryLock())
acquire(1);
}
}
}
}
來看下該方法的具體實現,簡而言之,該方法嘗試以獨占的方式獲取鎖。
static final class NonfairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) {
// first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}
}
先是通過 compareAndSetState(0, 1) 方法,以原子操作的方式將 AQS 類中的 state 變量值從 0 修改到 1。
我們在上文中提到過,state 變量來標識資源的鎖占用狀態,0 代表未占用,1 代表已占用,大于 1 則代表鎖被重入,那么該操作就是嘗試獲取鎖。
若該操作執行成功,則通過 setExclusiveOwnerThread(current) 作用是將當前線程設置為持有獨占鎖的線程,并返回 true,代表獲取鎖成功了。
再往下分析 getExclusiveOwnerThread() == current,這是判斷當前線程是否已獲取該鎖且處于未釋放的狀態,若判斷成立則將 state 變量+1代表重入,并返回 true 表示獲取鎖成功。
btw:從這段代碼邏輯上看,知道為什么叫非公平鎖了吧,一上來并沒有通過 AQS 排隊,而且先去爭搶鎖。
接下來我們繼續來看acquire(1)方法,代碼如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg))
acquire(null, arg, false, false, false, 0L);
}
}
方法體重有兩個方法加上一個判斷,先來看 tryAcquire(arg) 方法的執行邏輯。
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
這塊代碼的邏輯竟然跟上面 initialTryLock() 方法的前半段幾乎一樣,先是通過 compareAndSetState(0, 1) 方法將 AQS 類中的 state 變量值從 0 修改到 1。
若該操作執行成功,則通過 setExclusiveOwnerThread(current) 作用是將當前線程設置為持有獨占鎖的線程,并返回 true,代表獲取鎖成功了。
btw:果然是非公平鎖啊,這是誓要將插隊爭搶鎖進行到底了。
下面就是 AQS 中的重頭戲了,acquire(null, arg, false, false, false, 0L)方法,實現排隊獲取鎖。
代碼實現如下:
圖片
這塊代碼并非主業務鏈路,先是進行了三個判斷,當前節點不是 first 節點和 head 節點,且前驅結點不為null。
btw:head 節點可以理解為“當前持有獨占鎖的線程”,而在 head 節點之后的線程都處于阻塞、等待被喚醒的狀態,而 first 節點則是同步隊列中第一個等待獲取鎖的線程。
接下來 pred.status < 0 代表前驅節點已經被取消,結果為 true 則做一次等待隊列清理。
而 pred.prev == null 則是判斷前驅節點是否為 null,結果為 true 則跳到下一次循環中。
圖片
這段代碼的意思是,在當前節點為 first 節點或前驅節點為 null (未入隊)的情況下,繼續通過 tryAcquire(arg) 方法嘗試獲取鎖。
圖片
這段代碼看起來比較復雜,其實也是有邏輯性的。
1、前兩個大的邏輯分支判斷的意思是,先創建一個獨占節點,并將該節點加入到 CLH 隊列的尾部。
2、如果當前節點為 first 節點,且自旋數大于 0,則繼續嘗試自旋獲取鎖。
3、將當前節點的狀態值設置為“等待中”。
4、當前節點自旋失敗,使用 LockSupport.pack() 方法掛起線程。
5、當獨占鎖被釋放,隊列中的 first 節點的線程將被喚醒,清除當前節點的等待狀態。