JDK鎖的基礎--AQS實現原理(三)
阿新 • • 發佈:2019-01-11
本文主要來分析一下AQS共享模式鎖的獲取和釋放,AQS其實只是一個框架,它主要提供了一個int型別的state欄位,子類繼承時用於儲存子類的狀態,並且提供了一個等待佇列以及維護等待佇列的方法。至於如何使用這個狀態值和等待佇列,就需要子類根據自己的需求來實現了。
以Semaphore類為例,Semaphore允許多個執行緒同時獲得訊號量先來看一下Semaphore的介面:
//Semaphore
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1 );
}
同樣的,sync是一個定義在Semaphore中的AQS的抽象子類,在Semaphore類中有兩種實現,一個是公平的,一個是非公平的。轉到AQS中的acquireSharedInterruptibly
方法,
//AbstractQueuedSynchornizer
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//由於本文分析共享模式鎖,所以說tryAcquireShared嘗試獲取的是permit而不是鎖
//tryAcquireShared嘗試獲取相應數量的permit,如果失敗返回負值。返回0代表獲取成功但是下次呼叫會失敗,返回正值代表獲取成功而且下次呼叫可能也會成功
//可以理解為返回0代表只有0個permit,所以下次呼叫會失敗,而返回正值代表還有permit,所以下次呼叫可能會成功
if (tryAcquireShared(arg) < 0)
//獲取失敗後需要新建一個等待節點並將節點加入等待佇列
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//新建一個共享模式的節點並將其加入等待佇列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//如果其前驅節點是頭節點,那麼再次嘗試獲取permit
int r = tryAcquireShared(arg);
if (r >= 0) {
//如果獲取成功那麼將該節點設定成頭節點,並且如果r>0,代表還有剩餘的permit,所以如果該節點的後繼節點也是共享模式的,就把後繼節點也喚醒
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
來看一下setHeadAndPropagate
方法,這個方法和setHead
不同的地方在於它不僅設定了等待佇列的頭節點,並且檢查其後繼節點是否可能是共享模式節點,如果是,而且傳入的propagate
大於0或者頭節點設定了PROPAGATE
狀態,那麼需要呼叫doReleaseShared
方法來喚醒後繼節點。setHeadAndPropagate
方法的處理過程比較保守,可能會導致很多不必要的喚醒。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//如果propagate>0,代表有剩餘的permit,喚醒共享模式節點
//如果h.waitStatus = PROPAGATE,表示之前的某次呼叫暗示了permit有剩餘,所以需要喚醒共享模式節點
//由於PROPAGATE狀態可能轉化為SIGNAL狀態,所以直接使用h.waitStatus < 0來判斷
//如果現在的頭節點的waitStatus<0,喚醒
//如果現在的頭節點等於null,喚醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果後繼節點為null,whatever喚醒
if (s == null || s.isShared())
doReleaseShared();
}
}
可以看到setHeadAndPropagate
方法的原則是寧濫勿缺,反正doReleaseShared
方法會繼續後來的處理:
private void doReleaseShared() {
for (;;) {
Node h = head;
//如果頭節點不為空且頭節點不等於尾節點,亦即等待佇列中有執行緒在等待
//需要注意的是,等待佇列的頭節點是已經獲得了鎖的執行緒,所以如果等待佇列中只有一個節點,那就說明沒有執行緒阻塞在這個等待佇列上
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果頭節點的狀態是SIGNAL,代表需要喚醒後面的執行緒(SIGNAL狀態可以看做是後繼節點處於被阻塞中)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//喚醒後繼節點
unparkSuccessor(h);
}
//如果頭節點的狀態為0,說明後繼節點還沒有被阻塞,不需要立即喚醒
//把頭節點的狀態設定成PROPAGATE,下次呼叫setHeadAndPropagate的時候前任頭節點的狀態就會是PROPAGATE,就會繼續呼叫doReleaseShared方法把喚醒“傳播”下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果頭節點被修改了那麼繼續迴圈下去
if (h == head) // loop if head changed
break;
}
}
根據自己的思考總結一下,不保證正確性:
- AQS的等待佇列的頭節點在初始化的時候是個啞節點,其它時候代表已經獲取鎖的節點(獨佔模式)或者獲取了permit的節點(共享模式),設定了頭節點的執行緒已經可以執行臨界區程式碼了。也就是說,在共享模式下,獲得了permit的執行緒代表的節點可能被其它節點擠出等待佇列。總之,等待佇列從第二個節點開始才是正在等待的執行緒。
- AQS的等待佇列的節點類Node只有在其後繼節點被阻塞的情況下才會是
SIGNAL
狀態,所以SIGNAL
狀態代表其後繼節點正在阻塞中。 - AQS等待佇列節點的
PROPAGATE
狀態代表喚醒的行為需要傳播下去,當頭節點的後繼節點並未處於阻塞狀態時(可能是剛呼叫addWaiter
方法新增到佇列中還未來得及阻塞),就給頭節點設定這個標記,表示下次呼叫setHeadAndPropagate
函式時會把這個喚醒行為傳遞下去。 - 設定
PROPAGATE
狀態的意義主要在於,每次釋放permit都會呼叫doReleaseShared
函式,而該函式每次只喚醒等待佇列的第一個等待節點。所以在本次歸還的permit足夠多的情況下,如果僅僅依靠釋放鎖之後的一次doReleaseShared
函式呼叫,可能會導致明明有permit但是有些執行緒仍然阻塞的情況。所以在每個執行緒獲取到permit之後,會根據剩餘的permit來決定是否把喚醒傳播下去。但不保證被喚醒的執行緒一定能獲得permit。 - 共享模式下會導致很多次不必要的喚醒。