【編程架構實戰】——Java併發包基石-AQS詳解 原

目錄

1 基本實現原理

1.1 如何使用

1.2 設計思想

2 自定義同步器

2.1 同步器代碼實現

2.2 同步器代碼測試

3 源碼分析

3.1 Node結點

3.2 獨佔式

3.3 共享式

4 總結 

 Java併發包(JUC)中提供了很多併發工具,這其中,很多我們耳熟能詳的併發工具,譬如ReentrangLock、Semaphore,它們的實現都用到了一個共同的基類--AbstractQueuedSynchronizer,簡稱AQS。AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。當然,我們自己也能利用AQS非常輕鬆容易地構造出符合我們自己需求的同步器。

本章我們就一起探究下這個神奇的東東,並對其實現原理進行剖析理解

基本實現原理

AQS使用一個int成員變量來表示同步狀態,通過內置的FIFO隊列來完成獲取資源線程的排隊工作。

    private volatile int state;//共享變量,使用volatile修飾保證線程可見性

狀態信息通過procted類型的getState,setState,compareAndSetState進行操作

AQS支持兩種同步方式:

  1.獨佔式

2.共享式

這樣方便使用者實現不同類型的同步組件,獨佔式如ReentrantLock,共享式如Semaphore,CountDownLatch,組合式的如ReentrantReadWriteLock。總之,AQS爲使用提供了底層支撐,如何組裝實現,使用者可以自由發揮。

同步器的設計是基於模板方法模式的,一般的使用方式是這樣:

  1.使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源state的獲取和釋放)

2.將AQS組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。

這其實是模板方法模式的一個很經典的應用。

我們來看看AQS定義的這些可重寫的方法:

  protected boolean tryAcquire(int arg) : 獨佔式獲取同步狀態,試着獲取,成功返回true,反之爲false

protected boolean tryRelease(int arg) :獨佔式釋放同步狀態,等待中的其他線程此時將有機會獲取到同步狀態;

protected int tryAcquireShared(int arg) :共享式獲取同步狀態,返回值大於等於0,代表獲取成功;反之獲取失敗;

protected boolean tryReleaseShared(int arg) :共享式釋放同步狀態,成功爲true,失敗爲false

protected boolean isHeldExclusively() : 是否在獨佔模式下被線程佔用。

關於AQS的使用,我們來簡單總結一下:

如何使用

首先,我們需要去繼承AbstractQueuedSynchronizer這個類,然後我們根據我們的需求去重寫相應的方法,比如要實現一個獨佔鎖,那就去重寫tryAcquire,tryRelease方法,要實現共享鎖,就去重寫tryAcquireShared,tryReleaseShared;最後,在我們的組件中調用AQS中的模板方法就可以了,而這些模板方法是會調用到我們之前重寫的那些方法的。也就是說,我們只需要很小的工作量就可以實現自己的同步組件,重寫的那些方法,僅僅是一些簡單的對於共享資源state的獲取和釋放操作,至於像是獲取資源失敗,線程需要阻塞之類的操作,自然是AQS幫我們完成了。

設計思想

  對於使用者來講,我們無需關心獲取資源失敗,線程排隊,線程阻塞/喚醒等一系列複雜的實現,這些都在AQS中爲我們處理好了。我們只需要負責好自己的那個環節就好,也就是獲取/釋放共享資源state的姿勢T_T。很經典的模板方法設計模式的應用,AQS爲我們定義好頂級邏輯的骨架,並提取出公用的線程入隊列/出隊列,阻塞/喚醒等一系列複雜邏輯的實現,將部分簡單的可由使用者決定的操作邏輯延遲到子類中去實現即可。

自定義同步器

同步器代碼實現

上面大概講了一些關於AQS如何使用的理論性的東西,接下來,我們就來看下實際如何使用,直接採用JDK官方文檔中的小例子來說明問題

 

 

 

 

 

 

 

同步器代碼測試

測試下這個自定義的同步器,我們使用之前文章中做過的併發環境下a++的例子來說明問題(a++的原子性其實最好使用原子類AtomicInteger來解決,此處用Mutex有點大炮打蚊子的意味,好在能說明問題就好)

 

 

 TestMutex

測試結果:

加鎖前,a=279204
加鎖後,a=300000

源碼分析

 我們先來簡單描述下AQS的基本實現,前面我們提到過,AQS維護一個共享資源state,通過內置的FIFO來完成獲取資源線程的排隊工作。(這個內置的同步隊列稱爲"CLH"隊列)。該隊列由一個一個的Node結點組成,每個Node結點維護一個prev引用和next引用,分別指向自己的前驅和後繼結點。AQS維護兩個指針,分別指向隊列頭部head和尾部tail。

 

 

 

 其實就是個雙端雙向鏈表。

當線程獲取資源失敗(比如tryAcquire時試圖設置state狀態失敗),會被構造成一個結點加入CLH隊列中,同時當前線程會被阻塞在隊列中(通過LockSupport.park實現,其實是等待態)。當持有同步狀態的線程釋放同步狀態時,會喚醒後繼結點,然後此結點線程繼續加入到對同步狀態的爭奪中。

Node結點

 Node結點是AbstractQueuedSynchronizer中的一個靜態內部類,我們撿Node的幾個重要屬性來說一下

 

 

 

獨佔式

 獲取同步狀態--acquire()

來看看acquire方法,lock方法一般會直接代理到acquire上

1  public final void acquire(int arg) {
2         if (!tryAcquire(arg) &&
3             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4             selfInterrupt();
5     }

 我們來簡單理一下代碼邏輯:

 a.首先,調用使用者重寫的tryAcquire方法,若返回true,意味着獲取同步狀態成功,後面的邏輯不再執行;若返回false,也就是獲取同步狀態失敗,進入b步驟;

b.此時,獲取同步狀態失敗,構造獨佔式同步結點,通過addWatiter將此結點添加到同步隊列的尾部(此時可能會有多個線程結點試圖加入同步隊列尾部,需要以線程安全的方 式添加);

c.該結點以在隊列中嘗試獲取同步狀態,若獲取不到,則阻塞結點線程,直到被前驅結點喚醒或者被中斷。

addWaiter

 爲獲取同步狀態失敗的線程,構造成一個Node結點,添加到同步隊列尾部

 

 

 

 先cas快速設置,若失敗,進入enq方法

將結點添加到同步隊列尾部這個操作,同時可能會有多個線程嘗試添加到尾部,是非線程安全的操作。

以上代碼可以看出,使用了compareAndSetTail這個cas操作保證安全添加尾結點。

enq方法

 

 

 

 enq內部是個死循環,通過CAS設置尾結點,不成功就一直重試。很經典的CAS自旋的用法,我們在之前關於原子類的源碼分析中也提到過。這是一種樂觀的併發策略。

最後,看下acquireQueued方法

acquireQueued

 

 

 

acquireQueued內部也是一個死循環,只有前驅結點是頭結點的結點,也就是老二結點,纔有機會去tryAcquire;若tryAcquire成功,表示獲取同步狀態成功,將此結點設置爲頭結點;若是非老二結點,或者tryAcquire失敗,則進入shouldParkAfterFailedAcquire去判斷判斷當前線程是否應該阻塞,若可以,調用parkAndCheckInterrupt阻塞當前線程,直到被中斷或者被前驅結點喚醒。若還不能休息,繼續循環。

 shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire用來判斷當前結點線程是否能休息

 

 

 若shouldParkAfterFailedAcquire返回true,也就是當前結點的前驅結點爲SIGNAL狀態,則意味着當前結點可以放心休息,進入parking狀態了。parkAncCheckInterrupt阻塞線程並處理中斷。

 

 

 

至此,關於acquire的方法源碼已經分析完畢,我們來簡單總結下

a.首先tryAcquire獲取同步狀態,成功則直接返回;否則,進入下一環節;

b.線程獲取同步狀態失敗,就構造一個結點,加入同步隊列中,這個過程要保證線程安全;

c.加入隊列中的結點線程進入自旋狀態,若是老二結點(即前驅結點爲頭結點),纔有機會嘗試去獲取同步狀態;否則,當其前驅結點的狀態爲SIGNAL,線程便可安心休息,進入阻塞狀態,直到被中斷或者被前驅結點喚醒。

  釋放同步狀態--release()

 當前線程執行完自己的邏輯之後,需要釋放同步狀態,來看看release方法的邏輯

 

 

 

 unparkSuccessor:喚醒後繼結點 

 

 

 

release的同步狀態相對簡單,需要找到頭結點的後繼結點進行喚醒,若後繼結點爲空或處於CANCEL狀態,從後向前遍歷找尋一個正常的結點,喚醒其對應線程。

共享式

  共享式:共享式地獲取同步狀態。對於獨佔式同步組件來講,同一時刻只有一個線程能獲取到同步狀態,其他線程都得去排隊等待,其待重寫的嘗試獲取同步狀態的方法tryAcquire返回值爲boolean,這很容易理解;對於共享式同步組件來講,同一時刻可以有多個線程同時獲取到同步狀態,這也是“共享”的意義所在。其待重寫的嘗試獲取同步狀態的方法tryAcquireShared返回值爲int。

 

 

 

 

 1.當返回值大於0時,表示獲取同步狀態成功,同時還有剩餘同步狀態可供其他線程獲取;

2.當返回值等於0時,表示獲取同步狀態成功,但沒有可用同步狀態了;

3.當返回值小於0時,表示獲取同步狀態失敗。

獲取同步狀態--acquireShared

 

 

 

 doAcquireShared

 

 

 大體邏輯與獨佔式的acquireQueued差距不大,只不過由於是共享式,會有多個線程同時獲取到線程,也可能同時釋放線程,空出很多同步狀態,所以當排隊中的老二獲取到同步狀態,如果還有可用資源,會繼續傳播下去。

setHeadAndPropagate

 

 

 

釋放同步狀態--releaseShared

 

 

 

 doReleaseShared

 

 

代碼邏輯比較容易理解,需要注意的是,共享模式,釋放同步狀態也是多線程的,此處採用了CAS自旋來保證。

總結

關於AQS的介紹及源碼分析到此爲止了。

AQS是JUC中很多同步組件的構建基礎,簡單來講,它內部實現主要是狀態變量state和一個FIFO隊列來完成,同步隊列的頭結點是當前獲取到同步狀態的結點,獲取同步狀態state失敗的線程,會被構造成一個結點(或共享式或獨佔式)加入到同步隊列尾部(採用自旋CAS來保證此操作的線程安全),隨後線程會阻塞;釋放時喚醒頭結點的後繼結點,使其加入對同步狀態的爭奪中。

AQS爲我們定義好了頂層的處理實現邏輯,我們在使用AQS構建符合我們需求的同步組件時,只需重寫tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared幾個方法,來決定同步狀態的釋放和獲取即可,至於背後複雜的線程排隊,線程阻塞/喚醒,如何保證線程安全,都由AQS爲我們完成了,這也是非常典型的模板方法的應用。AQS定義好頂級邏輯的骨架,並提取出公用的線程入隊列/出隊列,阻塞/喚醒等一系列複雜邏輯的實現,將部分簡單的可由使用者決定的操作邏輯延遲到子類中去實現。 

 

給大家推薦一個程序員學習羣:863621962。羣裏有分享的視頻,還有思維導圖

羣公告有視頻,都是乾貨的,你可以下載來看。主要分享分佈式架構、高可擴展、高性能、高併發、性能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分佈式項目實戰學習架構師視頻。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章