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