Java并發(fā)框架中的AQS詳細(xì)解析
前言
之前說鎖的升級的時候,說到了自旋鎖會空轉(zhuǎn)幾次嘗試等待獲取資源,其實(shí)這一系列的動作是有一個規(guī)范的這個規(guī)范叫做同步發(fā)生器AbstractQueuedSynchronizer ,簡稱AQS。
同步發(fā)生器是用來構(gòu)建鎖用的,可以說是Java中同步組件的基礎(chǔ),在Java JDK的JUC包中:java.util.concurrent。
我們常用的ReentrantLock,Semaphore等等用的都是這樣一個架構(gòu),可以說是這些Lock工具的一個基礎(chǔ)。
AQS基本思想
AQS是一個隊(duì)列管理器,通過內(nèi)置的FIFO同步隊(duì)列去管理線程爭奪資源的。
FIFO就是先進(jìn)先出(Frist In First Out)的縮寫。
其實(shí)現(xiàn)核心是用到了CHL同步隊(duì)列,其核心思想就是:把每一個線程看作一個節(jié)點(diǎn)Node,然后給這些節(jié)點(diǎn)上加上前驅(qū)指針和后繼指針,這樣一來就可以用指針把這些線程節(jié)點(diǎn)(Thread Node)連接起來形成一個雙向鏈表,或者說雙向隊(duì)列。
除此之外,還有一個同步器節(jié)點(diǎn)(Synchronized Node),用來管理這些線程節(jié)點(diǎn)。
同步器節(jié)點(diǎn)也有兩個指針,第一個指針指向隊(duì)列首節(jié)點(diǎn),第二個指針指向隊(duì)列尾節(jié)點(diǎn)。
因此同步器節(jié)點(diǎn)是事實(shí)上的頭節(jié)點(diǎn)Head,下圖就是一個完整的CHL同步隊(duì)列示意圖。

注:CHL是人名簡稱沒啥具體意義。
AQS操作同步隊(duì)列
同步隊(duì)列有了,誰能拿到資源則是由一個狀態(tài)變量(state)來確定的。
當(dāng)state==0時,表示當(dāng)前資源沒有線程占用;當(dāng)state>=1時,表示當(dāng)前資源已經(jīng)被占用了,其他線程必須等待資源釋放。
假設(shè)有一個線程要使用資源,首先先會去檢查state變量獲取結(jié)果,如果state==0說明該線程可以使用請求資源,不需要排隊(duì),直接取出線程節(jié)點(diǎn)去執(zhí)行;如果state>=1,說明該資源已經(jīng)被前面的線程拿走了,就必須要排隊(duì)。

既然有個這個概念,所以說每一個線程節(jié)點(diǎn)都會做這樣的事情:獲取鎖和釋放鎖。
每個在隊(duì)列里面的縣城節(jié)點(diǎn),都會不斷地自旋,每次自旋結(jié)束都會嘗試獲取鎖,如果獲取不到那么繼續(xù)自旋。
由于是FIFO先進(jìn)先出這種公平模式,因此線程頭節(jié)點(diǎn)總會第一個獲取到鎖,以此類推。
這里所有的節(jié)點(diǎn)都在[ 嘗試獲取鎖 – 自旋 ] 這種狀態(tài)不斷地重復(fù)。但是由于使用FIFO模式,只有頭節(jié)點(diǎn)的自旋是有意義的,其他的就是在空轉(zhuǎn)。

這樣做有什么好處呢?假設(shè)我們把所有沒有獲取到資源的線程都掛起,這就必然要經(jīng)過用戶線程和核心(系統(tǒng))線程之間的切換,這種切換是非常耗時的。
由于CPU執(zhí)行的會很快,所以預(yù)期就是自旋幾次以后,就可以拿到想要的鎖,以規(guī)避線程之間的切換。
AQS 的用法
上面說過AbstractQueuedSynchronizer是一個框架,它能干什么用還得祭出官方文檔一探究竟,官方文檔很長,我們截取兩句最重要的:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. 這句話是對AbstractQueuedSynchronizer定義,翻譯過來就是說:為那些想要依賴于FIFO等待隊(duì)列的阻塞鎖和相關(guān)的同步器(semaphores, events, 等等) 提供一個實(shí)施框架。 Subclasses should be defined as non-public internal helper classes that are used to implement the synchronization properties of their enclosing class. 這句話是用法的概括:其子類應(yīng)該被定義為非共有的內(nèi)部幫助器(助手類),用于實(shí)現(xiàn)外部類的屬性同步。
說白了AbstractQueuedSynchronizer就是Java給開發(fā)人員提供一個獲取鎖和釋放鎖的模板,用來處理synchronized封鎖粒度過大的問題。
它的主要功能方法如下:
| Modifier and Type | Method | Description |
| void | acquire(int arg) | Acquires in exclusive mode, ignoring interrupts. 獨(dú)占模式獲取對象,忽略中斷。 |
| void | acquireShared(int arg) | Acquires in shared mode, ignoring interrupts. 共享模式獲取對象,忽略中斷。 |
| boolean | release(int arg) | Releases in exclusive mode.以獨(dú)占模式釋放對象。 |
| boolean | releaseShared(int arg) | Releases in shared mode.以共享模式釋放對象。 |
| protected boolean | tryAcquire(int arg) | Attempts to acquire in exclusive mode.試圖以獨(dú)占模式獲取鎖,這個就是自旋的方法,一直試探 |
| protected int | tryAcquireShared(int arg) | Attempts to acquire in shared mode. 試圖以共享模式獲取鎖。 |
說明:共享模式下,當(dāng)一個線程獲取了鎖,其他線程依然可讀取信息。獨(dú)占模式下,線程獨(dú)占了鎖,不許其他線程使用。
源碼解析AQS
上面已經(jīng)AQS原理和常用方法說完了,總要有一個地方體現(xiàn)吧。我們可以打開一個方法看看Java中AQS獲取鎖是不是和我們說的原理一致。首先進(jìn)入acquire()方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}進(jìn)入以后先判斷tryAcquire(arg)這里面只有一個拋異常不多說了。然后調(diào)用acquireQueued()方法,進(jìn)入。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //空轉(zhuǎn),自旋
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { //如果當(dāng)前是頭節(jié)點(diǎn),繼續(xù)嘗試獲取鎖,這也就是為啥只有頭節(jié)點(diǎn)才有意義
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果不是繼續(xù)等待
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}發(fā)現(xiàn)這個方法已經(jīng)要求傳入的參數(shù)就是Node,其實(shí)就是把要等待的Node繼續(xù)等待,那么返回上去,看看addWaiter()又寫了啥。
private Node addWaiter(Node mode) {
//把當(dāng)前線程改造稱為Node,
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}進(jìn)入就發(fā)現(xiàn)用構(gòu)建Node用的就是當(dāng)前線程Thread.currentThread(),這就是線程節(jié)點(diǎn)的由來。
實(shí)現(xiàn)一個鎖類
在Java的JUC包中同樣為了這樣一個模板提供了一個實(shí)現(xiàn)接口就是Lock接口,我們常用的ReentrantLock就是實(shí)現(xiàn)了這個接口構(gòu)建出來的,不妨按照這樣一個模板自己構(gòu)建一個加深理解。
首先看下Lock接口里面都有什么方法:
| Modifier and Type | Method | Description |
| void | lock() | Acquires the lock. 獲取鎖。 |
| void | lockInterruptibly() | Acquires the lock unless the current thread is interrupted. 獲取鎖,除非當(dāng)前線程被中斷。 |
| Condition | newCondition() | Returns a new Condition instance that is bound to this Lock instance. 返回一個綁定Condition實(shí)例的鎖。 |
| boolean | tryLock() | Acquires the lock only if it is free at the time of invocation. 當(dāng)調(diào)用時鎖為空閑,才能獲取鎖。嘗試獲取鎖。 |
| boolean | tryLock(long time, TimeUnit unit) | Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted.在給定時間內(nèi)處于空閑狀態(tài)且當(dāng)前線程沒有被中斷時,才能獲取鎖。嘗試獲取鎖+超時時間。 |
| void | unlock() | Releases the lock. 釋放鎖。 |
按照上面的模板,我們可以自己構(gòu)建一個鎖工具:
public class MyLock implements Lock {
private Helper helper = new Helper();
//按照官方文檔所說,構(gòu)建內(nèi)部幫助類
private class Helper extends AbstractQueuedSynchronizer {
//構(gòu)建嘗試獲取鎖的方法
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if (0 == state) {
//利用cas的原理修改state
if (compareAndSetState(0, arg)) {
//設(shè)置當(dāng)前線程擁有資源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}//同一個線程獲取被鎖住的資源時,直接分配給這個線程,實(shí)現(xiàn)可重入性
else if(getExclusiveOwnerThread()==Thread.currentThread()){ //刪除這個else if條件,就會變?yōu)橐粋€不可重入鎖
setState(getState()+arg);
return true;
}
return false;
}
//構(gòu)建嘗試釋放鎖的方法
@Override
protected boolean tryRelease(int arg) {
//arg是傳遞進(jìn)來的state的期望值
int state = getState() - arg;
//判斷釋放后狀態(tài)是否為0
if (0 == state) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//因?yàn)樾枰薷牡木€程就是當(dāng)前占有鎖的線程,所以此時直接重置是沒有線程安全問題的,也就是當(dāng)前線程獨(dú)占了資源state
setState(state);
return false;
}
public Condition newConditionObject(){
return new ConditionObject();
}
}
//加鎖方法
@Override
public void lock() {
helper.acquire(1); //AbstractQueuedSynchronizer原生方法
}
//鎖中斷
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1); //AbstractQueuedSynchronizer原生方法
}
//嘗試獲取鎖
@Override
public boolean tryLock() {
return helper.tryAcquire(1); //使用自己實(shí)現(xiàn)的tryAcquire()方法
}
//嘗試加鎖
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireNanos(1,unit.toNanos(time)); //AbstractQueuedSynchronizer原生方法
}
//釋放鎖
@Override
public void unlock() {
helper.tryRelease(1); //使用自己實(shí)現(xiàn)的tryRelease()方法
}
//條件
@Override
public Condition newCondition() {
return helper.newConditionObject(); //AbstractQueuedSynchronizer原生方法
}
}總結(jié)
到此AQS的內(nèi)容告一段落。這篇博客主要講了AQS的設(shè)計(jì)思想,以及操作同步隊(duì)列的方式,同時完成了一個簡單的鎖幫助類,希望能夠幫助大家更好的理解鎖這一個同步機(jī)制,以及由AQS架構(gòu)為基礎(chǔ)的各種鎖工具的內(nèi)部原理。
到此這篇關(guān)于Java并發(fā)框架中的AQS詳細(xì)解析的文章就介紹到這了,更多相關(guān)Java的AQS內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Comparator.comparing比較導(dǎo)致空指針異常的解決
這篇文章主要介紹了Java Comparator.comparing比較導(dǎo)致空指針異常的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot整合JavaCV實(shí)現(xiàn)視頻截取第N幀并保存圖片
這篇文章主要為大家詳細(xì)介紹了springboot如何整合JavaCV實(shí)現(xiàn)視頻截取第N幀并保存為圖片,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-08-08
關(guān)于java String中intern的深入講解
這篇文章主要給大家介紹了關(guān)于java String中intern的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
SpringBoot自定義FailureAnalyzer過程解析
這篇文章主要介紹了SpringBoot自定義FailureAnalyzer,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
SpringBoot執(zhí)行有返回值的異步任務(wù)問題
這篇文章主要介紹了SpringBoot執(zhí)行有返回值的異步任務(wù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
關(guān)于如何搭建CAS服務(wù)并將CAS項(xiàng)目導(dǎo)入IDEA
這篇文章主要介紹了關(guān)于如何搭建CAS服務(wù)并將CAS項(xiàng)目導(dǎo)入IDEA的問題,文中提供了詳細(xì)的圖文講解,需要的朋友可以參考下,如果有錯誤的地方還請指正2023-03-03

