AQS簡介

1、引言

JAVA內置的鎖(使用同步方法和同步塊)一直以來備受關注,其優勢是可以花最小的空間開銷創建鎖(因爲每個JAVA對象或者類都可以作爲鎖使用)和最少的時間開銷獲得鎖(單線程可以在最短時間內獲得鎖)。線程同步越來越多地被用在多處理器上,特別是在高併發的情況下,然而,JVM內置鎖表現一般,而且不支持任何公平策略。從JAVA 5開始在java.util.concurrent包中引入了有別於Synchronized的同步框架。

下面談談它的設計思路:

設計一個同步器至少應該具以下有兩種操作:一個獲取方法,如果當前狀態不允許,將一直阻塞這個線程;一個釋放方法,修改狀態,讓其他線程有運行的機會。併發包中並沒有爲同步器提供一個統一的API,獲取和釋放方法在不同的類中的名稱不同,比如獲取方法有:Lock.lock,Semaphore.acquire, CountDownLatch.await和FutureTask.get.這些方法一般都重載有多種版本:阻塞與非阻塞版本、支持超時、支持中斷。

java.util.concurrent包中有很多同步類,比如互斥鎖、讀寫鎖、信號量等,這些同步類幾乎都可以用不同方式來實現,但是如果這樣做,那麼這樣的項目充其量只能算一個二流工程。JSR166並沒有生搬硬套,而是建立了一個同步中心類AbstractQueuedSynchronizer(簡稱:AQS)的框架,其中提供了大量的同步操作,而且用戶還可以在此類的基礎上自定義自己的同步類。其設計目標主要有兩點:

1、提高可擴展性,用戶可以自定義自己的同步類

2、最大限度地提高吞吐量,提供自定義公平策略


2、設計和實現

同步器的設計比較直接,前面提到包含獲取和釋放兩個操作:
獲取操作過程如下:
while (synchronization state does not allow acquire) {
    enqueue current thread if not already queued;
    possibly block current thread;
}
dequeue current thread if it was queued;
釋放操作:
update synchronization state;
if (state may permit a blocked thread to acquire)
    unblock one or more queued threads;
要滿足以上兩個操作,需要以下3點來支持:
1、原子操作同步狀態;
2、阻塞或者喚醒一個線程;

3、內部應該維護一個隊列。


2.1同步狀態

AQS用的是一個32位的整型來表示同步狀態的,可以通過以下幾個方法來設置和修改這個狀態字段:getState(),setState(),compareAndSetState().這些方法都需要java.util.concurrent.atomic包的支持,採用CAS操作.將state設置爲32位整型是一個務實的決定,雖然JSR166提供了64位版本的原子操作,但它還是使用對象內部鎖來實現的,如果採用64位的state會導致同步器表現不良好。32位同步器滿足大部分應用,如果確實需要64位的狀態,可以使用AbstractQueuedLongSynchronizer類.AQS是一個抽象類,如果它的實現類想要想要擁有對獲取和釋放的控制權,那它必須實現tryAcquire和tryRelease兩個方法。
[html] view plain copy
 print?
  1. protected final int getState() {  
  2.     return state;  
  3. }  
  4.   
  5.   
  6. protected final void setState(int newState) {  
  7.     state = newState;  
  8. }  
  9.   
  10.   
  11. protected final boolean compareAndSetState(int expect, int update) {  
  12.     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
  13. }  
  14.   
  15.   
  16. protected boolean tryAcquire(int arg) {  
  17.     throw new UnsupportedOperationException();  
  18. }  
  19.   
  20.   
  21. protected boolean tryRelease(int arg) {  
  22.     throw new UnsupportedOperationException();  
  23. }  


2.2阻塞

JSR166以前還沒有好的阻塞和解除阻塞線程的API可以使用!只有Thread.suspend 和 Thread.resume,但這兩個方法已經被廢棄了,原因是有可能導致死鎖。如果一個線程擁有監視器然後調用 Thread.suspend 使自已阻塞,另一個線程試圖調用Thread.resume去喚醒它,那麼這個線程去獲取監視器時即出現死鎖。直到後來出現的LockSupport解決了這個問題,LockSupport.park可以阻塞一個線程,LockSupport.unpack可以解除阻塞,調用一次park,然後調用多次unpack只會喚醒一個線程,阻塞針對線程而不是針對同步器。特別的,如果一個線程在一個新的同步器上調用pack方法有可能立即返回,因爲可能有剩餘的unpack存在。雖然調用多次unpack是想徹底清除阻塞狀態,但這顯得很笨拙,而且不划算,更有效的做法是在多次park的時候纔多次unpark.


2.3隊列

同步框架最重要的是要有一個同步隊列,在這裏被嚴格限制爲FIFO隊列,因此這個同步框架不支持基於優先級的同步策略。同步隊列採用非阻塞隊列毋庸置疑,當時非阻塞隊列只有兩個可供選擇CLH隊列鎖和MCS隊列鎖.原始的CLH Lock僅僅使用自旋鎖,但是相對於MSC Lock它更容易處理cancel和timeout,所以選擇了CLH Lock。

CLH隊列鎖的優點是:進出隊快,無鎖,暢通無阻(即使在有競爭的情況下,總有一個線程總是能夠很快插入到隊尾);檢查是否有線程在等待也是很容易的(只需要檢查頭尾指針是否相同)。最後設計出來的變種CLH Lock和原始的CLH Lock有較大的差別:

1、爲了可以處理timeout和cancel操作,每個node維護一個指向前驅的指針。如果一個node的前驅被cancel,這個node可以前向移動使用前驅的狀態字段。

2、第二個變動是在每個node裏使用一個狀態字段去控制阻塞,而不是自旋。一個排隊的線程調用acquire,只有在通過了子類實現的tryAcquire才能返回,確保只有隊頭線程才允許調用tryAcquire。

3、另外還有一些微小的改動:head結點使用的是傀儡結點。

變種的CLH隊列如下圖所示:


2.4條件隊列

同步框架提供了一個ConditionObject,一般和Lock接口配合來支持互斥模型,它提供類似JVM同步器的操作。條件對象可以和其他同步器有效的整合,它修復了JVM內置同步器的不足:一個鎖可以有多個條件。條件結點內部也有一個狀態字段,條件結點是通過nextWaiter指針串起來的一個獨立的隊列。條件隊列中的線程在獲取鎖之前,必須先被transfer到同步隊列中去。transfer先斷開條件隊列的第一個結點,然後插入到同步隊列中,這個新插入到同步隊列中的結點和同步隊列中的結點一起排隊等待獲取鎖。


3、用法

AbstractQueuedSynchronizer是一個採用模板方法模式實現的同步器基類,子類只需要實現獲取和釋放方法。子類一般不直接用於同步控制,而是採用代理模式。因爲獲取和釋放方法一般是私有的,實現細節不必暴露出來,所以常用委派的方法來使用同步器類:在一個類的內部申請一個私有的AQS的子類,委派它的所有同步方法。
[java] view plain copy
 print?
  1. class Mutex {  
  2.     class Sync extends AbstractQueuedSynchronizer {  
  3.         public boolean tryAcquire(int ignore) {  
  4.             return compareAndSetState(01);  
  5.         }  
  6.         public boolean tryRelease(int ignore) {  
  7.             setState(0);   
  8.             return true;  
  9.         }  
  10.     }  
  11.   
  12.     private final Sync sync = new Sync();  
  13.   
  14.     public void lock() {   
  15.         sync.acquire(0);   
  16.     }  
  17.   
  18.     public void unlock() {   
  19.         sync.release(0);   
  20.     }  
  21. }  
java.util.concurrent包中的所有同步工具類都依賴於AQS,其類型程序結構圖如下:


AbstractQueuedSynchronizer類還提供了其他一些同步控制方法,包括超時和中斷版的獲取方法,還集成了獨佔模式的同步器,如acquireShared,tryReleaseShared等方法。


3.1控制公平

雖然這個隊列被設計爲FIFO,但並不意味着這個同步器一定是公平的,前面談到,在tryAcquire檢查之後再排隊。因此,新線程完全可以偷偷排在第一個線程前面。之所以不採用FIFO,有時候是想獲得更高的吞吐量,爲了減少等待時間,新到的線程與隊列頭部的線程一起公平競爭,如果新來的線程比隊頭的線程快,那麼這個新來的線程就獲取鎖。隊頭線程失去競爭會再次阻塞,它的繼任也將會被阻塞,但這樣能避免飢餓。

如果需要絕對公平,那很簡單,只需要在tryAcquire方法,不在隊頭返回false即可。檢查是否在隊頭可以使用getFirstQueuedThread方法。有一情況是,隊列是空的,同時有多個線程一擁而入,誰先搶到鎖就誰運行,這其實與公平並不衝突,是對公平的補充。


3.2同步器

JAVA併發框架是如何使用AQS的:
ReentrantLock類使用同步狀態來代表持有鎖的數量,當一個鎖被獲得,會記錄獲取該鎖的線程身份,如果一個非當前線程試圖釋放鎖是不合法的。該類也使用了ConditionObject類,和一些監視和檢查方法。該類支持公平與非公平兩種模式,是通過AQS的兩個子類來實現的。
ReentrantReadWriteLock類將32位的state分成高位和低位,16位用於寫鎖計數,其餘16位用於讀鎖計數。
Semaphore類使用同步狀態保持當前計數,acquireShared減少計數,tryRelease的增加計數,如果state是正數就喚醒線程。
CountDownLatch類使用同步狀態代表計數。所有線程都獲得鎖時,狀態爲0,就喚醒。

當然用戶可以定義自己的應用程序同步器。例如:事件,集中管理的鎖,基於樹的障礙等。

發佈了29 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章