轉:https://mp.weixin.qq.com/s/mykFjE-t9wbQBjG3xMzDWw
通過上一篇《Java 并發(2)AbstractQueuedSynchronizer 源碼分析之獨占模式》的分析,我們知道了獨占模式獲取鎖有三種方式,分別是不響應線程中斷獲取,響應線程中斷獲取,設置超時時間獲取。在共享模式下獲取鎖的方式也是這三種,而且基本上都是大同小異,我們搞清楚了一種就能很快的理解其他的方式。
雖然說 AbstractQueuedSynchronizer 源碼有一千多行,但是重復的也比較多,所以讀者不要剛開始的時候被嚇到,只要耐著性子去看慢慢的自然能夠漸漸領悟。就我個人經驗來說,閱讀 AbstractQueuedSynchronizer 源碼有幾個比較關鍵的地方需要弄明白,分別是獨占模式和共享模式的區別,結點的等待狀態,以及對條件隊列的理解。理解了這些要點那么后續源碼的閱讀將會輕松很多。
當然這些在《Java 并發(1)AbstractQueuedSynchronizer 源碼分析之概要分析》這篇文章里都有詳細的介紹,讀者可以先去查閱。本篇對于共享模式的分析也是分為三種獲取鎖的方式和一種釋放鎖的方式。
1、不響應線程中斷的獲取
//以不可中斷模式獲取鎖(共享模式)
//以不可中斷模式獲取鎖(共享模式)
publicfinalvoidacquireShared(intarg){
? //1.嘗試去獲取鎖
? if (tryAcquireShared(arg) < 0) {
? ? ? //2.如果獲取失敗就進入這個方法
? ? ? doAcquireShared(arg);
? }
}
//嘗試去獲取鎖(共享模式)
//負數:表示獲取失敗
//零值:表示當前結點獲取成功, 但是后繼結點不能再獲取了
//正數:表示當前結點獲取成功, 并且后繼結點同樣可以獲取成功
protectedinttryAcquireShared(intarg){
? throw new UnsupportedOperationException();
}
調用 acquireShared 方法是不響應線程中斷獲取鎖的方式。在該方法中,首先調用 tryAcquireShared 去嘗試獲取鎖,tryAcquireShared 方法返回一個獲取鎖的狀態,這里 AQS 規定了返回狀態若是負數代表當前結點獲取鎖失敗,若是 0 代表當前結點獲取鎖成功,但后繼結點不能再獲取了,若是正數則代表當前結點獲取鎖成功,并且這個鎖后續結點也同樣可以獲取成功。
子類在實現 tryAcquireShared 方法獲取鎖的邏輯時,返回值需要遵守這個約定。如果調用 tryAcquireShared 的返回值小于 0,就代表這次嘗試獲取鎖失敗了,接下來就調用 doAcquireShared 方法將當前線程添加進同步隊列。我們看到 doAcquireShared 方法。
//在同步隊列中獲取(共享模式)
privatevoiddoAcquireShared(intarg){
//添加到同步隊列中
finalNode node = addWaiter(Node.SHARED);
booleanfailed =true;
try{
booleaninterrupted =false;
for(;;) {
//獲取當前結點的前繼結點
finalNode p = node.predecessor();
//如果前繼結點為head結點就再次嘗試去獲取鎖
if(p == head) {
//再次嘗試去獲取鎖并返回獲取狀態
//r < 0, 表示獲取失敗
//r = 0, 表示當前結點獲取成功, 但是后繼結點不能再獲取了
//r > 0, 表示當前結點獲取成功, 并且后繼結點同樣可以獲取成功
intr = tryAcquireShared(arg);
if(r >=0) {
//到這里說明當前結點已經獲取鎖成功了, 此時它會將鎖的狀態信息傳播給后繼結點
setHeadAndPropagate(node, r);
p.next =null;
//如果在線程阻塞期間收到中斷請求, 就在這一步響應該請求
if(interrupted) {
selfInterrupt();
}
failed =false;
return;
}
}
//每次獲取鎖失敗后都會判斷是否可以將線程掛起, 如果可以的話就會在parkAndCheckInterrupt方法里將線程掛起
if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted =true;
}
}
}finally{
if(failed) {
cancelAcquire(node);
}
}
}
進入 doAcquireShared 方法首先是調用 addWaiter 方法將當前線程包裝成結點放到同步隊列尾部。這個添加結點的過程我們在講獨占模式時講過,這里就不再講了。
結點進入同步隊列后,如果它發現在它前面的結點就是 head 結點,因為 head 結點的線程已經獲取鎖進入房間里面了,那么下一個獲取鎖的結點就輪到自己了,所以當前結點先不會將自己掛起,而是再一次去嘗試獲取鎖,如果前面那人剛好釋放鎖離開了,那么當前結點就能成功獲得鎖,如果前面那人還沒有釋放鎖,那么就會調用 shouldParkAfterFailedAcquire 方法,在這個方法里面會將 head 結點的狀態改為 SIGNAL,只有保證前面結點的狀態為 SIGNAL,當前結點才能放心的將自己掛起,所有線程都會在 parkAndCheckInterrupt 方法里面被掛起。
如果當前結點恰巧成功的獲取了鎖,那么接下來就會調用 setHeadAndPropagate 方法將自己設置為 head 結點,并且喚醒后面同樣是共享模式的結點。下面我們看下 setHeadAndPropagate 方法具體的操作。
//設置head結點并傳播鎖的狀態(共享模式)
privatevoidsetHeadAndPropagate(Node node,intpropagate){
Node h = head;
//將給定結點設置為head結點
setHead(node);
//如果propagate大于0表明鎖可以獲取了
if(propagate >0|| h ==null|| h.waitStatus <0) {
//獲取給定結點的后繼結點
Node s = node.next;
//如果給定結點的后繼結點為空, 或者它的狀態是共享狀態
if(s ==null|| s.isShared()) {
//喚醒后繼結點
doReleaseShared();
}
}
}
//釋放鎖的操作(共享模式)
privatevoiddoReleaseShared(){
for(;;) {
//獲取同步隊列的head結點
Node h = head;
if(h !=null&& h != tail) {
//獲取head結點的等待狀態
intws = h.waitStatus;
//如果head結點的狀態為SIGNAL, 表明后面有人在排隊
if(ws == Node.SIGNAL) {
//先把head結點的等待狀態更新為0
if(!compareAndSetWaitStatus(h, Node.SIGNAL,0)) {
continue;
}
//再去喚醒后繼結點
unparkSuccessor(h);
//如果head結點的狀態為0, 表明此時后面沒人在排隊, 就只是將head狀態修改為PROPAGATE
}elseif(ws ==0&& !compareAndSetWaitStatus(h,0, Node.PROPAGATE)) {
continue;
}
}
//只有保證期間head結點沒被修改過才能跳出循環
if(h == head) {
break;
}
}
}
調用 setHeadAndPropagate 方法首先將自己設置成 head 結點,然后再根據傳入的 tryAcquireShared 方法的返回值來決定是否要去喚醒后繼結點。前面已經講到當返回值大于 0 就表明當前結點成功獲取了鎖,并且后面的結點也可以成功獲取鎖。
這時當前結點就需要去喚醒后面同樣是共享模式的結點,注意,每次喚醒僅僅只是喚醒后一個結點,如果后一個結點不是共享模式的話,當前結點就直接進入房間而不會再去喚醒更后面的結點了。共享模式下喚醒后繼結點的操作是在 doReleaseShared 方法進行的,共享模式和獨占模式的喚醒操作基本也是相同的,都是去找到自己座位上的牌子 (等待狀態),如果牌子上為 SIGNAL 表明后面有人需要讓它幫忙喚醒,如果牌子上為 0 則表明隊列此時并沒有人在排隊。
在獨占模式下是如果發現沒人在排隊就直接離開隊列了,而在共享模式下如果發現隊列后面沒人在排隊,當前結點在離開前仍然會留個小紙條 (將等待狀態設置為 PROPAGATE) 告訴后來的人這個鎖的可獲取狀態。那么后面來的人在嘗試獲取鎖的時候可以根據這個狀態來判斷是否直接獲取鎖。
2、響應線程中斷的獲取
//以可中斷模式獲取鎖(共享模式)
publicfinalvoidacquireSharedInterruptibly(intarg)throwsInterruptedException{
//首先判斷線程是否中斷, 如果是則拋出異常
if(Thread.interrupted()) {
thrownewInterruptedException();
}
//1.嘗試去獲取鎖
if(tryAcquireShared(arg) <0) {
//2. 如果獲取失敗則進人該方法
doAcquireSharedInterruptibly(arg);
}
}
//以可中斷模式獲取(共享模式)
privatevoiddoAcquireSharedInterruptibly(intarg)throwsInterruptedException{
//將當前結點插入同步隊列尾部
finalNode node = addWaiter(Node.SHARED);
booleanfailed =true;
try{
for(;;) {
//獲取當前結點的前繼結點
finalNode p = node.predecessor();
if(p == head) {
intr = tryAcquireShared(arg);
if(r >=0) {
setHeadAndPropagate(node, r);
p.next =null;
failed =false;
return;
}
}
if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
//如果線程在阻塞過程中收到過中斷請求, 那么就會立馬在這里拋出異常
thrownewInterruptedException();
}
}
}finally{
if(failed) {
cancelAcquire(node);
}
}
}
響應線程中斷獲取鎖的方式和不響應線程中斷獲取鎖的方式在流程上基本是相同的,唯一的區別就是在哪里響應線程的中斷請求。在不響應線程中斷獲取鎖時,線程從 parkAndCheckInterrupt 方法中被喚醒,喚醒后就立馬返回是否收到中斷請求,即使是收到了中斷請求也會繼續自旋直到獲取鎖后才響應中斷請求將自己給掛起。而響應線程中斷獲取鎖會才線程被喚醒后立馬響應中斷請求,如果在阻塞過程中收到了線程中斷就會立馬拋出 InterruptedException 異常。
3、設置超時時間的獲取
//以限定超時時間獲取鎖(共享模式)
publicfinalbooleantryAcquireSharedNanos(intarg,longnanosTimeout)throwsInterruptedException{
if(Thread.interrupted()) {
thrownewInterruptedException();
}
//1.調用tryAcquireShared嘗試去獲取鎖
//2.如果獲取失敗就調用doAcquireSharedNanos
returntryAcquireShared(arg) >=0|| doAcquireSharedNanos(arg, nanosTimeout);
}
//以限定超時時間獲取鎖(共享模式)
privatebooleandoAcquireSharedNanos(intarg,longnanosTimeout)throwsInterruptedException{
longlastTime = System.nanoTime();
finalNode node = addWaiter(Node.SHARED);
booleanfailed =true;
try{
for(;;) {
//獲取當前結點的前繼結點
finalNode p = node.predecessor();
if(p == head) {
intr = tryAcquireShared(arg);
if(r >=0) {
setHeadAndPropagate(node, r);
p.next =null;
failed =false;
returntrue;
}
}
//如果超時時間用完了就結束獲取, 并返回失敗信息
if(nanosTimeout <=0) {
returnfalse;
}
//1.檢查是否滿足將線程掛起要求(保證前繼結點狀態為SIGNAL)
//2.檢查超時時間是否大于自旋時間
if(shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) {
//若滿足上面兩個條件就將當前線程掛起一段時間
LockSupport.parkNanos(this, nanosTimeout);
}
longnow = System.nanoTime();
//超時時間每次減去獲取鎖的時間
nanosTimeout -= now - lastTime;
lastTime = now;
//如果在阻塞時收到中斷請求就立馬拋出異常
if(Thread.interrupted()) {
thrownewInterruptedException();
}
}
}finally{
if(failed) {
cancelAcquire(node);
}
}
}
如果看懂了上面兩種獲取方式,再來看設置超時時間的獲取方式就會很輕松,基本流程都是一樣的,主要是理解超時的機制是怎樣的。如果第一次獲取鎖失敗會調用 doAcquireSharedNanos 方法并傳入超時時間,進入方法后會根據情況再次去獲取鎖,如果再次獲取失敗就要考慮將線程掛起了。
這時會判斷超時時間是否大于自旋時間,如果是的話就會將線程掛起一段時間,否則就繼續嘗試獲取,每次獲取鎖之后都會將超時時間減去獲取鎖的時間,一直這樣循環直到超時時間用盡,如果還沒有獲取到鎖的話就會結束獲取并返回獲取失敗標識。在整個期間線程是響應線程中斷的。
4、共享模式下結點的出隊操作
//釋放鎖的操作(共享模式)
publicfinalbooleanreleaseShared(intarg){
//1.嘗試去釋放鎖
if(tryReleaseShared(arg)) {
//2.如果釋放成功就喚醒其他線程
doReleaseShared();
returntrue;
}
returnfalse;
}
//嘗試去釋放鎖(共享模式)
protectedbooleantryReleaseShared(intarg){
thrownewUnsupportedOperationException();
}
//釋放鎖的操作(共享模式)
privatevoiddoReleaseShared(){
for(;;) {
//獲取同步隊列的head結點
Node h = head;
if(h !=null&& h != tail) {
//獲取head結點的等待狀態
intws = h.waitStatus;
//如果head結點的狀態為SIGNAL, 表明后面有人在排隊
if(ws == Node.SIGNAL) {
//先把head結點的等待狀態更新為0
if(!compareAndSetWaitStatus(h, Node.SIGNAL,0)) {
continue;
}
//再去喚醒后繼結點
unparkSuccessor(h);
//如果head結點的狀態為0, 表明此時后面沒人在排隊, 就只是將head狀態修改為PROPAGATE
}elseif(ws ==0&& !compareAndSetWaitStatus(h,0, Node.PROPAGATE)) {
continue;
}
}
//只有保證期間head結點沒被修改過才能跳出循環
if(h == head) {
break;
}
}
}
線程在房間辦完事之后就會調用 releaseShared 方法釋放鎖,首先調用 tryReleaseShared 方法嘗試釋放鎖,該方法的判斷邏輯由子類實現。如果釋放成功就調用 doReleaseShared 方法去喚醒后繼結點。走出房間后它會找到原先的座位 (head 結點),看看座位上是否有人留了小紙條 (狀態為 SIGNAL),如果有就去喚醒后繼結點。如果沒有 (狀態為 0) 就代表隊列沒人在排隊,那么在離開之前它還要做最后一件事情,就是在自己座位上留下小紙條 (狀態設置為 PROPAGATE),告訴后面的人鎖的獲取狀態,整個釋放鎖的過程和獨占模式唯一的區別就是在這最后一步操作。