文章目錄
-
- (一)、概述
- (二)、共享鎖的擷取
- (三)、共享鎖的釋放
- (四)、總結
(一)、概述
這篇文章将會分析AQS源碼之共享鎖,我在上一篇文章中已經分析過AQS的獨占鎖以及AQS的底層實作原理,如果還沒有看過的朋友,建議先看一下,因為這樣了解共享鎖會更加容易。
共享鎖:允許在同一時刻多個線程持有鎖。
共享鎖的特點: 當一個共享鎖的線程被喚醒,那麼它同時也會喚醒在同步隊列中的下一個線程,如果這個線程也是等待共享鎖的話。但是如果下一個線程在等待的是獨占鎖的話,那是無法擷取的。
(二)、共享鎖的擷取
public final void acquireShared(int arg) {
//嘗試擷取鎖
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
首先來看這個方法,首先調用了tryAcquireShared方法,這同樣是一個protected方法,隻是簡單地抛出一個異常,需要子類進行具體的實作。tryAcquireShared()方法的作用是嘗試獲得共享鎖。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
接着調用了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);
//如果大于0,則需要喚醒後繼節點
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);
}
}
上面這個方法中特别需要注意的是tryAcquireShared(),這個方法會傳回一個數字。當傳回值小于0的時候,表示擷取鎖失敗。當傳回值等于0的時候,表示擷取鎖成功,但不喚醒後面的線程。當傳回值大于0的時候,需要嘗試喚醒後面等待共享鎖的線程。
下面來看一下setHeadAndPropagate()如何喚醒後面排隊的線程:
private void setHeadAndPropagate(Node node, int propagate) {
//獲得頭節點
Node h = head; // Record old head for check below
//将node節點設定為頭節點
setHead(node);
//這裡有兩種情況需要被喚醒
//1、propagate > 0,表示需要喚醒後繼節點
//h.waitStatus < 0,頭節點的後繼節點需要被喚醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//判斷是否存在後繼節點,以及後繼節點是否是在等待共享鎖,而不是獨占鎖的
if (s == null || s.isShared())
//喚醒,這個後面會詳細解釋
doReleaseShared();
}
}
上面的方法中通過判斷下一個節點是否為null以及是否是在等待共享鎖,進而調用doReleaseShared()方法,這個方法主要是用來喚醒其他線程的。
private void doReleaseShared() {
//死循環,自旋操作
for (;;) {
//獲得頭節點
Node h = head;
//判斷頭節點是否為null,以及是否隻有一個結點
if (h != null && h != tail) {
//如果不是,擷取頭節點的狀态
int ws = h.waitStatus;
//如果狀态是SIGNAL,那麼就是會喚醒下一個線程
if (ws == Node.SIGNAL) {
//CAS操作将狀态置為0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//執行喚醒操作
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;
}
}
上面執行喚醒操作調用了unparkSuccessor執行喚醒操作:
private void unparkSuccessor(Node node) {
//擷取節點的狀态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//将s指派為目前節點的下一個節點
Node s = node.next;
//如果節點s不需要被喚醒
if (s == null || s.waitStatus > 0) {
s = null;
//從後往前周遊,離頭節點最近的需要被喚醒的節點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//調用unpark()喚醒節點。
if (s != null)
LockSupport.unpark(s.thread);
}
(三)、共享鎖的釋放
public final boolean releaseShared(int arg) {
//釋放鎖
if (tryReleaseShared(arg)) {
//喚醒後面的線程
doReleaseShared();
return true;
}
return false;
}
從上面的方法中可以看到,先嘗試釋放鎖,如果釋放成功,就嘗試喚醒下一個在等待共享鎖的線程。
(四)、總結
其實共享鎖和獨占鎖的實作原理都是差不多。共享鎖的主要特征在于當一個在等待隊列中的共享節點成功擷取到鎖以後(它擷取到的是共享鎖),既然是共享,那它必須要依次喚醒後面所有可以跟它一起共享目前鎖資源的節點,毫無疑問,這些節點必須也是在等待共享鎖(這是大前提,如果等待的是獨占鎖,那前面已經有一個共享節點擷取鎖了,它肯定是擷取不到的)。
參考資料:AQS之共享鎖