AQS初體驗
AQS是AbstractQueuedSynchronizer的簡稱。AQS提供了一種實現阻塞鎖和一系列依賴FIFO等待佇列的同步器的框架。所謂框架,AQS使用了模板方法的設計模式,為我們遮蔽了諸如內部佇列等一系列複雜的操作,讓我們專注於對鎖相關功能的實現。
獲取鎖
既然涉及到鎖競爭的問題,必然需要一個標誌位來表示鎖的狀態,AQS中提供了state這樣一個成員變數,為了安全的操作state,我們需要使用原子操作。將state從0修改為1就代表這個執行緒已經持有了這把鎖。
但競爭鎖的執行緒絕對不會只是一個,其他未競爭到鎖的執行緒該如何進行處理?
第一個答案可能是重試,重試雖好,但是可不能貪杯,如果競爭很嚴重,無數的執行緒在不斷的重新嘗試獲取鎖,我們的CPU早晚會吃不消。
第二個比較好的方式就是排隊,持有鎖的執行緒釋放鎖之後,通知下一個執行緒去獲取鎖,避免了不必要的CPU損失。但是值得注意的是,即使是從佇列中被喚醒的執行緒去獲取鎖也依舊可能獲取不到的,因為無時無刻都有新加入的執行緒來競爭鎖。
AQS實際上就是使用了雙端佇列來解決了這個問題的。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()如果失敗將執行acquireQueued()中的addWaiter()方法,即嘗試加入等待佇列。這個等待佇列使用了雙端佇列進行實現,在AQS中定義了一個Node的資料結構,AQS中維護著head和tail兩個成員變數。
在單執行緒中插入佇列尾部很簡單,只需要將原來的tail的next指向新插入的節點,並且將tail重新設定為新插入的節點。但是在多執行緒環境中,很有可能發生多個執行緒同時插入尾部的現象,而上述的插入過程不具有原子性,同時插入的過程必將出現多個操作順序的混亂,最終導致等待佇列的tail節點
AQS在插入tail節點時使用原子操作來保證了插入的可靠。
private Node addWaiter(Node mode) {
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;
}
插入成功的直接返回了node,而沒有插入成功的則執行了enq()函式,在enq()中使用了CAS進行插入。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
經歷這個CAS插入,最後全部的節點都將被插入到佇列尾部。
現在,沒有獲取到鎖的執行緒已經被放進佇列了,但是放入佇列也代表著我們可以忘了初心。我們的目標是獲取鎖,而不是進入佇列。acquireQueued()就在嘗試為我們獲取鎖。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
簡單說就是,檢查自己是不是head節點的下一個節點,如果是的話,嘗試獲取獲取鎖;如果不是的話,將使用LockSupport的park方法阻塞當前執行緒,避免造成CPU的浪費。
釋放鎖
釋放鎖的過程可以分成兩大部分:
- 恢復AQS的狀態為無鎖狀態
- 喚醒等待佇列中下一個等待的節點
在第一個過程中,沒有排在隊頭的節點都已經被阻塞了,而喚醒的時機就是前一個節點已經釋放鎖,所以可以說這個等待佇列,實際上是一個喚醒鏈。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 使用unpark喚醒下一個執行緒
unparkSuccessor(h);
return true;
}
return false;
}
總結
AQS為我們提供了:
- status 狀態同步標示
- Node雙端佇列 儲存競爭鎖執行緒
- 基於Node雙端佇列的執行緒喚醒機制
我覺得AQS精華在於,將原來N個執行緒併發競爭鎖降低為1+M(新加入)個。在我們自己實現類似的資源競爭演算法中,也可以通過加入佇列來降低競爭的併發度,降低CPU的負載壓力。