我在Java併發之AQS原始碼分析(一)這篇文章中,從原始碼的角度深度剖析了 AQS 獨佔鎖模式下的獲取鎖與釋放鎖的邏輯,如果你把這部分搞明白了,再看共享鎖的實現原理,思路就會清晰很多。下面我們繼續從原始碼中窺探共享鎖的實現原理。
共享鎖
獲取鎖
public final void acquireShared(int arg) {
// 嘗試獲取共享鎖,小於0表示獲取失敗
if (tryAcquireShared(arg) < 0)
// 執行獲取鎖失敗的邏輯
doAcquireShared(arg);
}
複製程式碼
這裡的 tryAcquireShared 方法是留給實現方去實現獲取鎖的具體邏輯的,我們主要看 doAcquireShared 方法的實現邏輯:
private void doAcquireShared(int arg) {
// 新增共享鎖型別節點到佇列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次嘗試獲取共享鎖
int r = tryAcquireShared(arg);
// 如果在這裡成功獲取共享鎖,會進入共享鎖喚醒邏輯
if (r >= 0) {
// 共享鎖喚醒邏輯
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 與獨佔鎖相同的掛起邏輯
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
看到上面的程式碼,是不是有一種熟悉的感覺,同樣是採用了自旋機制,線上程掛起之前,不斷地迴圈嘗試獲取鎖,不同的是,一旦獲取共享鎖,會呼叫 setHeadAndPropagate 方法同時喚醒後繼節點,實現共享模式,下面是喚醒後繼節點程式碼邏輯:
private void setHeadAndPropagate(Node node, int propagate) {
// 頭節點
Node h = head;
// 設定當前節點為新的頭節點
// 這裡不需要加鎖操作,因為獲取共享鎖後,會從FIFO佇列中依次喚醒佇列,並不會產生併發安全問題
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 後繼節點
Node s = node.next;
// 如果後繼節點為空或者後繼節點為共享型別,則進行喚醒後繼節點
// 這裡後繼節點為空意思是隻剩下當前頭節點了
if (s == null || s.isShared())
doReleaseShared();
}
}
複製程式碼
該方法主要做了兩個重要的步驟:
- 將當前節點設定為新的頭節點,這點很重要,這意味著當前節點的前置節點(舊頭節點)已經獲取共享鎖了,從佇列中去除;
- 呼叫 doReleaseShared 方法,它會呼叫 unparkSuccessor 方法喚醒後繼節點。
釋放鎖
public final boolean releaseShared(int arg) {
// 由使用者自行實現釋放鎖條件
if (tryReleaseShared(arg)) {
// 執行釋放鎖
doReleaseShared();
return true;
}
return false;
}
複製程式碼
下面是釋放鎖邏輯:
private void doReleaseShared() {
for (;;) {
// 從頭節點開始執行喚醒操作
// 這裡需要注意,如果從setHeadAndPropagate方法呼叫該方法,那麼這裡的head是新的頭節點
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示後繼節點需要被喚醒
if (ws == Node.SIGNAL) {
// 初始化節點狀態
//這裡需要CAS原子操作,因為setHeadAndPropagate和releaseShared這兩個方法都會頂用doReleaseShared,避免多次unpark喚醒操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 如果初始化節點狀態失敗,繼續迴圈執行
continue; // loop to recheck cases
// 執行喚醒操作
unparkSuccessor(h);
}
//如果後繼節點暫時不需要喚醒,那麼當前頭節點狀態更新為PROPAGATE,確保後續可以傳遞給後繼節點
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果在喚醒的過程中頭節點沒有更改,退出迴圈
// 這裡防止其它執行緒又設定了頭節點,說明其它執行緒獲取了共享鎖,會繼續迴圈操作
if (h == head) // loop if head changed
break;
}
}
複製程式碼
共享鎖的釋放鎖邏輯比獨佔鎖的釋放鎖邏輯稍微複雜,原因是共享鎖需要釋放佇列中所有共享型別的節點,因此需要迴圈操作,由於釋放鎖過程中會涉及多個地方修改節點狀態,此時需要 CAS 原子操作來併發安全。
獲取共享鎖流程圖:
總結
更獨佔鎖相比,從流程圖也可看出,共享鎖的主要特徵是當有一個執行緒獲取到鎖之後,那麼它就會依次喚醒等待佇列中可以跟它共享的節點,當然這些節點也是共享鎖型別。