搞清楚AQS獨占鎖的實作原理之后,再看共享鎖的實作原理就會輕松很多,兩種鎖模式之間很多通用的地方本文只會簡單說明一下,就不在贅述了
一、執行程序概述
獲取鎖的程序:
- 當執行緒呼叫acquireShared()申請獲取鎖資源時,如果成功,則進入臨界區,
- 當獲取鎖失敗時,則創建一個共享型別的節點并進入一個FIFO等待佇列,然后被掛起等待喚醒,
- 當佇列中的等待執行緒被喚醒以后就重新嘗試獲取鎖資源,如果成功則喚醒后面還在等待的共享節點并把該喚醒事件傳遞下去,即會依次喚醒在該節點后面的所有共享節點,然后進入臨界區,否則繼續掛起等待,
釋放鎖程序:
- 當執行緒呼叫releaseShared()進行鎖資源釋放時,如果釋放成功,則喚醒佇列中等待的節點,如果有的話,
二、原始碼深入分析
基于上面所說的共享鎖執行流程,我們接下來看下原始碼實作邏輯:
首先來看下獲取鎖的方法acquireShared(),如下
public final void acquireShared(int arg) {
//嘗試獲取共享鎖,回傳值小于0表示獲取失敗
if (tryAcquireShared(arg) < 0)
//執行獲取鎖失敗以后的方法
doAcquireShared(arg);
}
這里tryAcquireShared()方法是留給用戶去實作具體的獲取鎖邏輯的,關于該方法的實作有兩點需要特別說明:
一、該方法必須自己檢查當前背景關系是否支持獲取共享鎖,如果支持再進行獲取,
二、該方法回傳值是個重點,其一、由上面的原始碼片段可以看出回傳值小于0表示獲取鎖失敗,需要進入等待佇列,其二、如果回傳值等于0表示當前執行緒獲取共享鎖成功,但它后續的執行緒是無法繼續獲取的,也就是不需要把它后面等待的節點喚醒,最后、如果回傳值大于0,表示當前執行緒獲取共享鎖成功且它后續等待的節點也有可能繼續獲取共享鎖成功,也就是說此時需要把后續節點喚醒讓它們去嘗試獲取共享鎖,
有了上面的約定,我們再來看下doAcquireShared方法的實作:
//引數不多說,就是傳給acquireShared()的引數
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表示不用喚醒后繼節點,大于0需要
if (r >= 0) {
//這里是重點,獲取到鎖以后的喚醒操作,后面詳細說
setHeadAndPropagate(node, r);
p.next = null;
//如果是因為中斷醒來則設定中斷標記位
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//掛起邏輯跟獨占鎖一樣,不再贅述
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//獲取失敗的取消邏輯跟獨占鎖一樣,不再贅述
if (failed)
cancelAcquire(node);
}
}
獨占鎖模式獲取成功以后設定頭結點然后回傳中斷狀態,結束流程,而共享鎖模式獲取成功以后,呼叫了setHeadAndPropagate方法,從方法名就可以看出除了設定新的頭結點以外還有一個傳遞動作,一起看下代碼:
//兩個入參,一個是當前成功獲取共享鎖的節點,一個就是tryAcquireShared方法的回傳值,注意上面說的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //記錄當前頭節點
//設定新的頭節點,即把當前獲取到鎖的節點設定為頭節點
//注:這里是獲取到鎖之后的操作,不需要并發控制
setHead(node);
//這里意思有兩種情況是需要執行喚醒操作
//1.propagate > 0 表示呼叫方指明了后繼節點需要被喚醒
//2.頭節點后面的節點需要被喚醒(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();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
最終的喚醒操作也很復雜,專門拿出來分析一下:
注:這個喚醒操作在releaseShare()方法里也會呼叫,
private void doReleaseShared() {
for (;;) {
//喚醒操作由頭結點開始,注意這里的頭節點已經是上面新設定的頭結點了
//其實就是喚醒上面新獲取到共享鎖的節點的后繼節點
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后繼節點需要被喚醒
if (ws == Node.SIGNAL) {
//這里需要控制并發,因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//執行喚醒操作
unparkSuccessor(h);
}
//如果后繼節點暫時不需要喚醒,則把當前節點狀態設定為PROPAGATE確保以后可以傳遞下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果頭結點沒有發生變化,表示設定完成,退出回圈
//如果頭結點發生變化,比如說其他執行緒獲取到了鎖,為了使自己的喚醒動作可以傳遞,必須進行重試
if (h == head)
break;
}
}
接下來看下釋放共享鎖的程序:
public final boolean releaseShared(int arg) {
//嘗試釋放共享鎖
if (tryReleaseShared(arg)) {
//喚醒程序,詳情見上面分析
doReleaseShared();
return true;
}
return false;
}
注:上面的setHeadAndPropagate()方法表示等待佇列中的執行緒成功獲取到共享鎖,這時候它需要喚醒它后面的共享節點(如果有),但是當通過releaseShared()方法去釋放一個共享鎖的時候,接下來等待獨占鎖跟共享鎖的執行緒都可以被喚醒進行嘗試獲取,
三、總結
跟獨占鎖相比,共享鎖的主要特征在于當一個在等待佇列中的共享節點成功獲取到鎖以后(它獲取到的是共享鎖),既然是共享,那它必須要依次喚醒后面所有可以跟它一起共享當前鎖資源的節點,毫無疑問,這些節點必須也是在等待共享鎖(這是大前提,如果等待的是獨占鎖,那前面已經有一個共享節點獲取鎖了,它肯定是獲取不到的),當共享鎖被釋放的時候,可以用讀寫鎖為例進行思考,當一個讀鎖被釋放,此時不論是讀鎖還是寫鎖都是可以競爭資源的,
歡迎關注公眾號 【碼農開花】一起學習成長
我會一直分享Java干貨,也會分享免費的學習資料課程和面試寶典
回復:【計算機】【設計模式】【面試】有驚喜哦
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/248857.html
標籤:Java
