Java面經(jīng)之AQS原理
點擊藍字關注我們,獲取更多面經(jīng)

AQS(AbstractQueuedSynchronizer)即 隊列同步器,是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承。子類推薦被定義為自定義同步組件的靜態(tài)內(nèi)部類,同步器自身沒有實現(xiàn)任何同步接口,它僅僅是定義了若干同步狀態(tài)獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態(tài),也可以支持共享式地獲取同步狀態(tài)。
同步器的設計是基于模板方法模式的,也就是說,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現(xiàn)中,并調(diào)用同步器提供的模板方法,而這些模板方法將會調(diào)用使用者重寫的方法。重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態(tài)。
·getState():獲取當前同步狀態(tài)。
·setState(int newState):設置當前同步狀態(tài)。
·compareAndSetState(int expect,int update):使用CAS設置當前狀態(tài),該方法能夠保證狀態(tài)設置的原子性。
同步器提供的模板方法基本上分為3類:獨占式獲取與釋放同步狀態(tài)、共享式獲取與釋放同步狀態(tài)和查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法來實現(xiàn)自己的同步語義。
下面看下 同步器可被重寫的方法如下(如,ReentrantLock就重寫了下面的某些方法)
同步器可重寫的方法如圖:

下面我們就舉個例子,看獨占鎖(獨占鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處于同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖)是如何利用AQS,和重寫他的方法來達到獨占鎖的效果的。
public class Mutex implements Lock {// 靜態(tài)內(nèi)部類,自定義同步器private static class Sync extends AbstractQueuedSynchronizer {// 是否處于占用狀態(tài)protected boolean isHeldExclusively() {return getState() == 1;}// 當狀態(tài)為0的時候獲取鎖public boolean tryAcquire(int acquires) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}// 釋放鎖,將狀態(tài)設置為0protected boolean tryRelease(int releases) {if (getState() == 0) throw newIllegalMonitorStateException();setExclusiveOwnerThread(null);setState(0);return true;}// 返回一個Condition,每個condition都包含了一個condition隊列Condition newCondition() { return new ConditionObject(); }}// 僅需要將操作代理到Sync上即可private final Sync sync = new Sync();public void lock() { sync.acquire(1); }public boolean tryLock() { return sync.tryAcquire(1); }public void unlock() { sync.release(1); }public Condition newCondition() { return sync.newCondition(); }public boolean isLocked() { return sync.isHeldExclusively(); }public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}}
Mutex中定義了一個靜態(tài)內(nèi)部類,該內(nèi)部類繼承了同步器并實現(xiàn)了獨占式獲取和釋放同步狀態(tài)。在tryAcquire(int acquires)方法中,如果經(jīng)過CAS設置成功(同步狀態(tài)設置為1),則代表獲取了同步狀態(tài),而在tryRelease(int releases)方法中只是將同步狀態(tài)重置為0。用戶使用Mutex時并不會直接和內(nèi)部同步器的實現(xiàn)打交道,而是調(diào)用Mutex提供的方法,在Mutex的實現(xiàn)中,以獲
取鎖的lock()方法為例,只需要在方法實現(xiàn)中調(diào)用同步器的模板方法acquire(int args)即可。
下面我們隊隊列同步器AQS的實現(xiàn)進行分析。主要包括:同步隊列、獨占式同步狀態(tài)獲取與釋放、共享式同步狀態(tài)獲取與釋放以及超時獲取同步狀態(tài)等同步器的核心數(shù)據(jù)結構與模板方法。
同步器依賴內(nèi)部的同步隊列(一個FIFO雙向隊列)來完成同步狀態(tài)的管理,當前線程獲取同步狀態(tài)失敗時,同步器會將當前線程以及等待狀態(tài)等信息構造成為一個節(jié)點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態(tài)釋放時,會把首節(jié)點中的線程喚醒,使其再次嘗試獲取同步狀態(tài)。節(jié)點(Node)用來保存獲取同步狀態(tài)失敗的線程引用、等待狀態(tài)以及前驅和后繼節(jié)點,具體的定義和狀態(tài)大家可以自行去看下源碼這里略過。同步隊列的基本結構如圖所示:

同步器包含了兩個節(jié)點類型的引用,一個指向頭節(jié)點,而另一個指向尾節(jié)點。試想一下,當一個線程成功地獲取了同步狀態(tài)(或者鎖),其他線程將無法獲取到同步狀態(tài),轉而被構造成為節(jié)點并加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基于CAS的設置尾節(jié)點的方法:compareAndSetTail(Node expect,Nodeupdate),它需要傳遞當前線程“認為”的尾節(jié)點和當前節(jié)點,只有設置成功后,當前節(jié)點才正式與之前的尾節(jié)點建立關聯(lián)。
同步隊列遵循FIFO,首節(jié)點是獲取同步狀態(tài)成功的節(jié)點,首節(jié)點的線程在釋放同步狀態(tài)時,將會喚醒后繼節(jié)點,而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設置為首節(jié)點。
下面分別看下獨占式同步狀態(tài)獲取與釋放和共享式同步狀態(tài)獲取與釋放是如何實現(xiàn)的。
1)獨占式同步狀態(tài)獲取與釋放
通過調(diào)用同步器的acquire(int arg)方法可以獲取同步狀態(tài),該方法對中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進入同步隊列中,后續(xù)對線程進行中斷操作時,線程不會從同步隊列中移出,該方法代碼如下:

上述代碼主要完成了同步狀態(tài)獲取、節(jié)點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調(diào)用自定義同步器實現(xiàn)的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態(tài),如果同步狀態(tài)獲取失敗,則構造同步節(jié)點并通過addWaiter(Node node)方法將該節(jié)點加入到同步隊列的尾部,最后調(diào)用acquireQueued(Node node,int arg)方法,使得該節(jié)點以“死循環(huán)”的方式獲取同步狀態(tài)。如果獲取不到則阻塞節(jié)點中的線程,而被阻塞線程的喚醒主要依靠前驅節(jié)點的出隊或阻塞線程被中斷來實現(xiàn)。
我們看下同步器的addWaiter和enq的實現(xiàn)代碼如下:

上述代碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節(jié)點能夠被線程安全添加。
在enq(final Node node)方法中,同步器通過“死循環(huán)”來保證節(jié)點的正確添加,在“死循環(huán)”中只有通過CAS將節(jié)點設置成為尾節(jié)點之后,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。
節(jié)點進入同步隊列之后,就進入了一個自旋的過程(acquireQueued方法),每個節(jié)點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態(tài),就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節(jié)點的線程),如代碼所示

在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環(huán)”中嘗試獲取同步狀態(tài),而只有前驅節(jié)點是頭節(jié)點才能夠嘗試獲取同步狀態(tài)。

獨占式同步狀態(tài)獲取流程,也就是acquire(int arg)方法調(diào)用流程,如圖所示

下面看下如何釋放釋放同步狀態(tài)的:

該方法執(zhí)行時,會喚醒頭節(jié)點的后繼節(jié)點線程,unparkSuccessor(Node node)方法使用LockSupport(這里不做介紹)來喚醒處于等待狀態(tài)的線程。
適當做個總結:在獲取同步狀態(tài)時,同步器維護一個同步隊列,獲取狀態(tài)失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節(jié)點為頭節(jié)點且成功獲取了同步狀態(tài)。在釋放同步狀態(tài)時,同步器調(diào)用tryRelease(int arg)方法釋放同步狀態(tài),然后喚醒頭節(jié)點的后繼節(jié)點。
2)共享式同步狀態(tài)獲取與釋放
共享式獲取與獨占式獲取最主要的區(qū)別在于同一時刻能否有多個線程同時獲取到同步狀態(tài)。
通過調(diào)用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態(tài),該方法代碼如下:

在acquireShared(int arg)方法中,同步器調(diào)用tryAcquireShared(int arg)方法嘗試獲取同步狀態(tài),tryAcquireShared(int arg)方法返回值為int類型,當返回值大于等于0時,表示能夠獲取到同步狀態(tài)。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態(tài)并退出自旋的條件就是tryAcquireShared(int arg)方法返回值大于等于0??梢钥吹剑赿oAcquireShared(int arg)方法的自旋過程中,如果當前節(jié)點的前驅為頭節(jié)點時,嘗試獲取同步狀態(tài),如果返回值大于等于0,表示該次獲取同步狀態(tài)成功并從自旋過程中退出.
與獨占式一樣,共享式獲取也需要釋放同步狀態(tài),通過調(diào)用releaseShared(int arg)方法可以釋放同步狀態(tài),該方法代碼:

該方法在釋放同步狀態(tài)之后,將會喚醒后續(xù)處于等待狀態(tài)的節(jié)點。對于能夠支持多個線程同時訪問的并發(fā)組件(比如Semaphore),它和獨占式主要區(qū)別在于tryReleaseShared(int arg)方法必須確保同步狀態(tài)(或者資源數(shù))線程安全釋放,一般是通過循環(huán)和CAS來保證的,因為釋放同步狀態(tài)的操作會同時來自多個線程
3)獨占式超時獲取同步狀態(tài)
通過調(diào)用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態(tài),即在指定的時間段內(nèi)獲取同步狀態(tài),如果獲取到同步狀態(tài)則返回true,否則,返回false。該方法提供了傳統(tǒng)Java同步操作(比如synchronized關鍵字)所不具備的特性。
超時獲取同步狀態(tài)過程可以被視作響應中斷獲取同步狀態(tài)過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大于0則表示超時時間未到,需要繼續(xù)睡眠nanosTimeout納秒,反之,表示已經(jīng)超時.具體代碼這里就不貼出來了。大致的流程圖如下:

獨占式超時獲取同步狀態(tài)doAcquireNanos(int arg,long nanosTimeout)和獨占式獲取同步狀態(tài)acquire(int args)在流程上非常相似,其主要區(qū)別在于未獲取到同步狀態(tài)時的處理邏輯。acquire(int args)在未獲取到同步狀態(tài)時,將會使當前線程一直處于等待狀態(tài),而doAcquireNanos(int arg,long nanosTimeout)會使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內(nèi)沒有獲取到同步狀態(tài),將會從等待邏輯中自動返回。
更多面經(jīng)
掃描二維碼
獲取更多面經(jīng)
扶搖就業(yè)
