Java并發(fā)框架中的AQS詳細(xì)解析
前言
之前說鎖的升級的時候,說到了自旋鎖會空轉(zhuǎn)幾次嘗試等待獲取資源,其實這一系列的動作是有一個規(guī)范的這個規(guī)范叫做同步發(fā)生器AbstractQueuedSynchronizer ,簡稱AQS。
同步發(fā)生器是用來構(gòu)建鎖用的,可以說是Java中同步組件的基礎(chǔ),在Java JDK的JUC包中:java.util.concurrent。
我們常用的ReentrantLock,Semaphore等等用的都是這樣一個架構(gòu),可以說是這些Lock工具的一個基礎(chǔ)。
AQS基本思想
AQS是一個隊列管理器,通過內(nèi)置的FIFO同步隊列去管理線程爭奪資源的。
FIFO就是先進(jìn)先出(Frist In First Out)的縮寫。
其實現(xiàn)核心是用到了CHL同步隊列,其核心思想就是:把每一個線程看作一個節(jié)點Node,然后給這些節(jié)點上加上前驅(qū)指針和后繼指針,這樣一來就可以用指針把這些線程節(jié)點(Thread Node)連接起來形成一個雙向鏈表,或者說雙向隊列。
除此之外,還有一個同步器節(jié)點(Synchronized Node),用來管理這些線程節(jié)點。
同步器節(jié)點也有兩個指針,第一個指針指向隊列首節(jié)點,第二個指針指向隊列尾節(jié)點。
因此同步器節(jié)點是事實上的頭節(jié)點Head,下圖就是一個完整的CHL同步隊列示意圖。
注:CHL是人名簡稱沒啥具體意義。
AQS操作同步隊列
同步隊列有了,誰能拿到資源則是由一個狀態(tài)變量(state)來確定的。
當(dāng)state==0時,表示當(dāng)前資源沒有線程占用;當(dāng)state>=1時,表示當(dāng)前資源已經(jīng)被占用了,其他線程必須等待資源釋放。
假設(shè)有一個線程要使用資源,首先先會去檢查state變量獲取結(jié)果,如果state==0說明該線程可以使用請求資源,不需要排隊,直接取出線程節(jié)點去執(zhí)行;如果state>=1,說明該資源已經(jīng)被前面的線程拿走了,就必須要排隊。
既然有個這個概念,所以說每一個線程節(jié)點都會做這樣的事情:獲取鎖和釋放鎖。
每個在隊列里面的縣城節(jié)點,都會不斷地自旋,每次自旋結(jié)束都會嘗試獲取鎖,如果獲取不到那么繼續(xù)自旋。
由于是FIFO先進(jìn)先出這種公平模式,因此線程頭節(jié)點總會第一個獲取到鎖,以此類推。
這里所有的節(jié)點都在[ 嘗試獲取鎖 – 自旋 ] 這種狀態(tài)不斷地重復(fù)。但是由于使用FIFO模式,只有頭節(jié)點的自旋是有意義的,其他的就是在空轉(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等待隊列的阻塞鎖和相關(guān)的同步器(semaphores, events, 等等) 提供一個實施框架。 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)部幫助器(助手類),用于實現(xiàn)外部類的屬性同步。
說白了AbstractQueuedSynchronizer就是Java給開發(fā)人員提供一個獲取鎖和釋放鎖的模板,用來處理synchronized封鎖粒度過大的問題。
它的主要功能方法如下:
Modifier and Type | Method | Description |
void | acquire(int arg) | Acquires in exclusive mode, ignoring interrupts. 獨占模式獲取對象,忽略中斷。 |
void | acquireShared(int arg) | Acquires in shared mode, ignoring interrupts. 共享模式獲取對象,忽略中斷。 |
boolean | release(int arg) | Releases in exclusive mode.以獨占模式釋放對象。 |
boolean | releaseShared(int arg) | Releases in shared mode.以共享模式釋放對象。 |
protected boolean | tryAcquire(int arg) | Attempts to acquire in exclusive mode.試圖以獨占模式獲取鎖,這個就是自旋的方法,一直試探 |
protected int | tryAcquireShared(int arg) | Attempts to acquire in shared mode. 試圖以共享模式獲取鎖。 |
說明:共享模式下,當(dāng)一個線程獲取了鎖,其他線程依然可讀取信息。獨占模式下,線程獨占了鎖,不許其他線程使用。
源碼解析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é)點,繼續(xù)嘗試獲取鎖,這也就是為啥只有頭節(jié)點才有意義 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,其實就是把要等待的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é)點的由來。
實現(xiàn)一個鎖類
在Java的JUC包中同樣為了這樣一個模板提供了一個實現(xiàn)接口就是Lock接口,我們常用的ReentrantLock就是實現(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實例的鎖。 |
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; } }//同一個線程獲取被鎖住的資源時,直接分配給這個線程,實現(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; } //因為需要修改的線程就是當(dāng)前占有鎖的線程,所以此時直接重置是沒有線程安全問題的,也就是當(dāng)前線程獨占了資源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); //使用自己實現(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); //使用自己實現(xiàn)的tryRelease()方法 } //條件 @Override public Condition newCondition() { return helper.newConditionObject(); //AbstractQueuedSynchronizer原生方法 } }
總結(jié)
到此AQS的內(nèi)容告一段落。這篇博客主要講了AQS的設(shè)計思想,以及操作同步隊列的方式,同時完成了一個簡單的鎖幫助類,希望能夠幫助大家更好的理解鎖這一個同步機(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-07springboot整合JavaCV實現(xiàn)視頻截取第N幀并保存圖片
這篇文章主要為大家詳細(xì)介紹了springboot如何整合JavaCV實現(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-04SpringBoot自定義FailureAnalyzer過程解析
這篇文章主要介紹了SpringBoot自定義FailureAnalyzer,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11SpringBoot執(zhí)行有返回值的異步任務(wù)問題
這篇文章主要介紹了SpringBoot執(zhí)行有返回值的異步任務(wù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07關(guān)于如何搭建CAS服務(wù)并將CAS項目導(dǎo)入IDEA
這篇文章主要介紹了關(guān)于如何搭建CAS服務(wù)并將CAS項目導(dǎo)入IDEA的問題,文中提供了詳細(xì)的圖文講解,需要的朋友可以參考下,如果有錯誤的地方還請指正2023-03-03