title: AQS原理機制淺探
date: 2019-11-18 16:14:00
tags:
- AQS
AQS原理機制(with源碼)
源碼探AQS原理機制
AQS(AbstractQueuedSynchronizer)是concurrent包下用來構建鎖或其他同步組件(信號量、事件等)的基礎框架類。 JDK中許多併發工具類的內部實現都依賴於AQS,如ReentrantLock, Semaphore, CountDownLatch等等。
自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
- isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。
- tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
- tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
- tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。
todo:按調用順序分方法拷貝出await()、signal()等源碼添加閱讀中文註釋,包括方法頭上 1. 2. 3.步總結做了什麼操作,然後在方法內關鍵代碼行上也添加閱讀註釋,參考如下
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UCHysqbt-1580121420273)(https://i.imgur.com/v4SH0J6.png)]
Condition原理:
await():
signal():
確認持有獨佔鎖後調用enq()將等待隊列中的Node節點移動到同步器同步隊列中,並且通過LockSupport.unpark(node.thread)喚醒節點中的線程參與同步隊列中獲取同步狀態(即鎖)的競爭中,同時await()中因不再滿足判斷不在同步隊列中條件,退出while循環,acquireQueued()成功獲取到同步狀態(鎖)後繼續往下執行,從await()方法返回,原線程不再阻塞繼續往下執行
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
//signal()移動到同步隊列後不滿足條件退出while循環
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued()成功獲取到同步狀態(鎖)後繼續往下執行,從await()方法返回,原線程不再阻塞繼續往下執行
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
CLH自旋鎖能確保無飢餓性,提供先來先服務的公平性。互斥鎖資源被佔用時會引起調用者睡眠,而自旋鎖不會 會一直循環獲取。
列舉JDK中幾種常見使用了AQS的同步組件:
- ReentrantLock: 使用了AQS的獨佔獲取和釋放,用state變量記錄某個線程獲取獨佔鎖的次數,獲取鎖時+1,釋放鎖時-1,在獲取時會校驗線程是否可以獲取鎖。
- Semaphore: 使用了AQS的共享獲取和釋放,用state變量作爲計數器,只有在大於0時允許線程進入。獲取鎖時-1,釋放鎖時+1。用作控制併發的數量,如數據庫連接池等需要限制數量的場景,如大小爲10的線程池限制DB連接數最大爲3
private static Semaphore semaphore = new Semaphore(3);
。 - CountDownLatch: 使用了AQS的共享獲取和釋放,用state變量作爲計數器,在初始化時指定。只要state還大於0,獲取共享鎖會因爲失敗而阻塞,直到計數器的值爲0時,共享鎖才允許獲取,所有等待線程會被逐一喚醒。
ReentrantLock獨佔模式獲取鎖過程:
tryAcquire()嘗試進行加鎖
先getState判斷狀態,如果狀態爲0即爲無鎖狀態。
如果爲公平鎖,則先判斷是否需要排隊,判斷依據爲head節點的下一個非取消(waitStatus<=0)節點不爲null且節點中的thread線程不爲null。如果不需要排隊返回false,if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
,取反true則直接嘗試CAS變更state狀態值加鎖,如果加鎖成功則設置獨佔線程爲當前線程。並且tryAcquire()返回true表示加鎖成功,不需要再進行下面的入隊排隊等
需要排隊時不用進行&&後面的CAS操作,不執行ifelse中代碼而是繼續往下判斷state狀態
如果狀態不爲0,即爲加鎖狀態,則判斷當前擁有獨佔鎖的線程是否爲當前線程
如果是,則將state值再加1(因爲此時持有鎖,所以直接 setState(c)
不用CAS)。這也是爲什麼ReentrantLock可重入,相應的重入鎖釋放鎖過程每次將state減1,減爲0時完全釋放鎖。
如果不是,則直接返回false表示加鎖失敗,進行入隊排隊。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
//設置node的前節點爲tail
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
//CAS替換tail爲當前node,成功之後將舊tail節點的next指針指向當前節點,注意這兩步之間可能有其他線程成爲新的tail節點,此時next指針還未加上,所以比如釋放鎖時要從tail往前prev遍歷纔不會漏掉節點,enq()方法中同樣如此
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
/**
* Initializes head and tail fields on first contention.
* 當第一次爭奪鎖時初始化同步隊列,也就是說,當第一個線程進來時隊列未初始化,也不會初始化,因爲此時沒有競爭,
* 看代碼料想第一個之後的後續直接獲取到鎖的線程也是不會初始化同步隊列
*/
private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
參考文章:
AbstractQueuedSynchronizer源碼解讀