在瞭解如何加鎖時候,我們再來了解如何解鎖。可重入互斥鎖ReentrantLock的解鎖方法unlock()並不區分是公平鎖還是非公平鎖,Sync類並沒有實現release(int arg)方法,這裡會實現呼叫其父類AbstractQueuedSynchronizer的release(int arg)方法。在release(int arg)方法中,會先呼叫其子類實現的tryRelease(int arg)方法,這裡我們看看ReentrantLock.Sync.tryRelease(int releases)的實現。
正常情況下,這段程式碼只能由佔有鎖的執行緒呼叫,所以這裡會先獲取鎖的引用計數,再減去釋放次數,即:c = getState() - releases,然後判斷嘗試釋放鎖的執行緒,是否是獨佔鎖的執行緒,如果不是則丟擲IllegalMonitorStateException異常。如果獨佔執行緒在釋放鎖後鎖的引用計數為0,則設定獨佔執行緒為null,再設定state為0,代表鎖成為無主狀態。
如果確定鎖成為無主狀態後,release(int arg)會檢查佇列中是否有需要喚醒的執行緒,如果頭節點header為null,則代表除了釋放鎖的執行緒,沒有任何執行緒搶鎖,如果頭節點的等待狀態為0,代表頭節點目前沒有需要喚醒的後繼節點。如果頭節點不為null且等待狀態不為0,則代表佇列中可能存在需要喚醒的執行緒,會進而執行unparkSuccessor(Node node),將頭節點作為引數傳入。
當我們將頭節點傳入unparkSuccessor(Node node),如果判斷頭節點的等待狀態<0,則會將頭節點的等待狀態設定為0,如果頭節點的後繼節點不為null,或者後繼節點尚未被取消,會直接喚醒後繼節點,後繼節點會退出parkAndCheckInterrupt()方法,acquireQueued(final Node node, int arg)的迴圈中重新競爭鎖,如果競爭成功,則後繼節點成為頭節點,退出搶鎖邏輯開始訪問資源,如果競爭失敗,這裡最多迴圈兩次執行shouldParkAfterFailedAcquire(Node pred, Node node),第一次先將後繼節點的前驅節點的等待狀態由0改為SIGNAL(1),表示前驅節點的後繼節點處於等待喚醒狀態,第二次迴圈如果還是搶鎖失敗,shouldParkAfterFailedAcquire(Node pred, Node node)判斷前驅節點的等待狀態為SIGNAL,返回true,後繼節點的執行緒陷入阻塞。需要注意的是,即便是公平鎖,也可能存在不公平的情況,公平鎖頭節點的後繼節點,也有可能存在搶鎖失敗的情況,比如之前說過,公平鎖在呼叫tryLock()時是不保證公平的。
這裡我們需要注意一下,後繼節點是可能存在被移除的情況,這種情況會在後續講解tryLock(long timeout, TimeUnit unit)的時候說明,如果一旦出現後繼節點被移除的情況(waitStatus > 0),在喚醒後繼節點時,會從尾節點向前驅節點遍歷,找到最靠近頭節點的有效節點。
public class ReentrantLock implements Lock, java.io.Serializable { //... private final Sync sync; //... abstract static class Sync extends AbstractQueuedSynchronizer { //... protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } //... } //... public void unlock() { sync.release(1); } //... } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //... public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } //... //由子類實現嘗試解鎖方法。 protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } //... private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) node.compareAndSetWaitStatus(ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //如果後繼節點不為null但被移除,會從尾節點向前驅節點遍歷,找到最靠前的有效節點 for (Node p = tail; p != node && p != null; p = p.prev) if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); } //... }
可重入互斥鎖的tryLock()方法不分公平鎖或非公平鎖,統一呼叫ReentrantLock.Sync.nonfairTryAcquire(int acquires),在JUC長征篇之ReentrantLock原始碼解析(一)已經介紹過nonfairTryAcquire(int acquires)方法了,這裡就不再贅述了。
public class ReentrantLock implements Lock, java.io.Serializable { //... private final Sync sync; //... abstract static class Sync extends AbstractQueuedSynchronizer { //... final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } //... } //... public boolean tryLock() { return sync.nonfairTryAcquire(1); } //... }
呼叫ReentrantLock.tryLock(long timeout, TimeUnit unit)方法時,會呼叫Sync父類AQS的tryAcquireNanos(int arg, long nanosTimeout)方法,如果執行緒已被標記為中斷,則會進入<1>處的分支並丟擲InterruptedException異常,否則先呼叫tryAcquire(int acquires)嘗試搶奪,如果搶鎖失敗,則會呼叫doAcquireNanos(int arg, long nanosTimeout)陷入計時阻塞,如果在阻塞期間內執行緒被中斷,則會丟擲InterruptedException異常,如果到達有效期後執行緒還未獲得鎖,tryAcquireNanos(int arg, long nanosTimeout)將返回false。
當進入doAcquireNanos(int arg, long nanosTimeout)後,會先在<2>處計算獲取鎖的截止時間,之後和原先的acquireQueued()很像,先把當前執行緒封裝成一個Node物件並加入到等待佇列,再判斷當前節點的前驅節點是否是頭節點,是的話再嘗試搶鎖,如果搶鎖成功則退出。否則用截止時間減去系統當前時間,算出執行緒剩餘的搶鎖時間(nanosTimeout)。如果剩餘時間<=0的話,代表到達截止時間後執行緒依舊未佔用鎖,於是呼叫<3>處的代表將執行緒對應的Node節點從等待佇列中移除,返回false表示搶鎖失敗。如果在後面的迴圈中如果發現當前時間大於截止時間,而執行緒還未獲得鎖,代表搶鎖失敗。
如果剩餘時間大於0,則會呼叫shouldParkAfterFailedAcquire(),如果前驅節點的狀態狀態為0,則將前驅節點的等待狀態設定為-1表示其後繼節點等待喚醒,然後在下一次迴圈的時候,shouldParkAfterFailedAcquire()判斷前置節點的等待狀態為-1,就有機會阻塞當前執行緒。但相比acquireQueued()不同的是,這裡會判斷剩餘時間是否大於1000納秒,如果剩餘時間小於等於1000納秒的話,這裡就不會阻塞執行緒,而是用自旋的方式,直到搶鎖成功,或者鎖超時搶鎖失敗。如果自旋期間或者阻塞期間執行緒被中斷,則會在<6>處丟擲InterruptedException異常。
public class ReentrantLock implements Lock, java.io.Serializable { //... private final Sync sync; //... public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } //... } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { static final long SPIN_FOR_TIMEOUT_THRESHOLD = 1000L; //... public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted())//<1> throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } //... private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout;//<2> final Node node = addWaiter(Node.EXCLUSIVE); try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) { cancelAcquire(node);//<3> return false; } if (shouldParkAfterFailedAcquire(p, node) &&//<4> nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)//<5> LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException();//<6> } } catch (Throwable t) { cancelAcquire(node); throw t; } } //... }
下面,我們來看看AQS是如何移除一個節點的,當要移除一個節點時,會先置空它的執行緒引用,再在<1>處遍歷當前節點的前驅節點,直到找到有效節點(等待狀態<=0),找到最靠近當前節點的有效節點pred後,再找到有效節點的後繼節點predNext(pred.next),然後我們將當前節點的等待狀態置為CANCELLED(1),如果當前節點是尾節點,則用CAS的方式設定尾節點為有效節點pred,如果pred成功設定為為節點,這裡還會用CAS的方式設定pred的後繼為null,因為尾節點不應該有後繼節點,這裡用CAS設定尾節點的後繼節點是防止pred成為尾節點後,還未置空尾節點的後繼節點前,又有新的節點入隊成為新的尾節點,並且設定pred的後繼節點為最新的尾節點。如果<2>出的程式碼執行成功,代表當前沒有新的節點入隊,如果執行失敗,代表有新的節點入隊,pred已經不是尾節點,且pred的後繼已經被修改。
如果node不是尾節點,或者在執行compareAndSetTail(node, pred)的時候,有新節點入隊,tail引用指向的物件已經不是node本身,或者在執行compareAndSetTail()執行失敗,則會進入<3>處的分支。進入<3>處的分支後,會先判斷當前節點的前驅節點是不是頭節點,如果是頭節點的話,會進入<5>處的分支,喚醒當前節點的後繼節點來競爭鎖,如果當前節點的後繼節點為null或者被取消的話,則會從尾節點開始遍歷前驅節點,找到佇列最前的有效節點,喚醒有效節點競爭鎖。
如果前驅節點不是頭節點,且前驅節點的等待狀態為SIGNAL或者前驅節點的等待狀態為有效狀態(waitStatus<=0)且成功設定前驅節點的等待狀態為SIGNAL,再判斷前驅節點前驅節點的thread引用是否為null,如果不為null則代表前驅節點還不是頭節點或者尚未被取消,此時就可以進入<4>處的分支,這裡如果判斷當前節點的後繼節點不為null或者尚未被取消,則用CAS的方式設定前驅節點的後繼節點,為當前節點的後繼節點。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //... private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; while (pred.waitStatus > 0)//<1> node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary, although with // a possibility that a cancelled node may transiently remain // reachable. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { pred.compareAndSetNext(predNext, null);//<2> } else {//<3> // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.thread != null) {//<4> Node next = node.next; if (next != null && next.waitStatus <= 0) pred.compareAndSetNext(predNext, next); } else {//<5> unparkSuccessor(node); } node.next = node; // help GC } } //... }
先前我們說過,會存在後繼節點指向被取消節點的情況,就是發生在cancelAcquire(Node node)方法裡,下圖是一個鎖的等待佇列,N1是隊頭,N1所對應的執行緒T1正佔有鎖進行資源訪問,N2和N5呼叫lock()方法採用非計時阻塞請求鎖,除非N2和N5對應的執行緒獲取到鎖,否則將永遠阻塞;N3和N4呼叫tryLock(long timeout, TimeUnit unit)方法採用計時阻塞請求鎖,如果超時對應的執行緒還未獲取到鎖,N3和N4將會從佇列中移除,返回搶鎖失敗。
我們假定N3和N4已經超時,要從佇列中移除,看看併發場景下是如何出現有效節點的後繼引用指向無效節點,這裡筆者稍微簡化cancelAcquire(Node node),我們只要專注可能出現有效節點指向無效節點的程式碼。
假定N3和N4兩個節點對應的執行緒是T3和T4,T3要從等待佇列中移除N3,先獲取N3的前驅節點pred(N3)為N2,N2是一個有效節點(waitStatus<=0),所以不需要在<1>處遍歷,接著在<2>處獲取N2的後繼節點predNext(N2)為N3,再將N3的等待狀態改為CANCELLED,此時T3掛起,T4開始執行。T4執行緒同樣獲取N4前驅節點pred(n4)為N3,然後發現N3的等待狀態>0,會一直往前遍歷到N2,所以N4的前驅節點pred(N4)會為N2,接著T4執行<2>處的程式碼,predNext(N2)也是N3,此時T4掛起,T3恢復執行。T3判斷N3不是尾節點,於是進入分值<4>,T3判斷N3的前驅節點N2不是頭節點,N2的狀態為為SIGNAL,且N2的thread欄位不為空,表明N2既沒有被取消,也不是頭節點,於是進入<5>處的分值,這裡獲取N3的後繼節點N4,由於N4對應的執行緒T4尚未執行到<3>處的程式碼,N4的等待狀態依舊為SIGNAL,所以T3會進入<6>處的分支,將N2的後繼節點指向N4。此時T4開始執行,將N4的等待狀態改為CANCELLED,T4進行<4>處的分支,N4的前驅N2不是頭節點,等待狀態為SIGNAL,且N2的執行緒引用不為怒null,繼而進入<5>處的分值,獲取到N4的後繼N5,N5不Wie空,且N5的等待狀態<=0,進而進入到<6>分支,最後要用CAS的方式嘗試將N2的後繼節點設定為N5,但這裡的設定一定會失敗,因為此時N2的後繼節點為N4,而T4原先獲取到N2的後繼節點為N3,出現了有效節點指向無效節點的情況。
private void cancelAcquire(Node node) { //... Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev;//<1> Node predNext = pred.next;//<2> node.waitStatus = Node.CANCELLED;//<3> if (node == tail && compareAndSetTail(node, pred)) { //... } else {//<4> // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.thread != null) {//<5> Node next = node.next; if (next != null && next.waitStatus <= 0)//<6> pred.compareAndSetNext(predNext, next); } else { //... } //... } }
最後等待佇列的佈局如下所示,N2指向無效節點N4,而非N4的後繼節點。同時這裡也引出另一個問題,哪怕N4不是無效節點,在上面移除節點的程式碼中,只設定了N2的後繼引用指向N4,卻沒設定N4的前驅引用指向N2,所以這裡N4的前驅依舊指向N3。
那麼如果真的出現上述這樣的情況,ReentrantLock是如何來修復這個佇列呢?答案在釋放鎖的時候呼叫unparkSuccessor(Node node)。筆者先前在介紹這個方法時,就有意提到後繼節點可能指向無效節點,當N1對應的執行緒使用完鎖釋放之後,N2對應的執行緒T2接著使用鎖並釋放鎖,在N2釋放鎖的時候,發現N2的後繼節點已經成為失效節點(waitStatus > 0),這裡會從尾節點開始找到佇列中最前面的有效節點,然後將其喚醒,這裡也就是N5。
private void unparkSuccessor(Node node) { //... Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node p = tail; p != node && p != null; p = p.prev) if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); }
N5在被喚醒後,會呼叫shouldParkAfterFailedAcquire(Node pred, Node node)發現原先的前驅節點N4的等待狀態處於被移除,會進入<1>處的分支,查詢到最靠近自己的有效前驅節點,並將前驅節點的後繼節點指向自己。這裡也能回答我們先前的問題,為何在要移除一個節點時,只修改前驅節點的後繼引用為被移除節點的後繼,卻不將被移除節點的後繼節點的前驅引用,指向其前驅,因為在釋放鎖的時候,會喚醒頭節點的後繼,如果被喚醒的後繼發現自己的前驅已經被移除,會往前查詢最靠近自己的有效前驅,這裡一般是頭節點,接著將頭節點的後繼引用指向自己,再往後就是我們熟悉的流程了,如果前驅節點是頭節點且搶鎖成功,則退出acquireQueued()方法進行資源的訪問,如果搶鎖失敗,則最多執行兩次shouldParkAfterFailedAcquire()然後陷入阻塞,等待下一次的喚醒。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //... if (ws > 0) {//<1> /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //... } return false; }
我們已經瞭解了lcok()、tryLock()以及tryLock(long timeout, TimeUnit unit),下面的lockInterruptibly()就變得尤為簡單,lockInterruptibly()顧名思義也是無限期陷入阻塞,直到獲得鎖或者被中斷,如果執行緒本身被中斷,在搶鎖時會直接丟擲InterruptedException異常,否則就開始搶鎖,如果搶鎖成功則皆大歡喜,搶鎖失敗則執行doAcquireInterruptibly(int arg),這個方法相信不需要筆者做過多的介紹,基本上很多步驟和方法在上面已經介紹過了。將執行緒封裝成Node節點併入隊,判斷Node節點的前驅是否是頭節點,是的話則試圖搶鎖,如果不是的話則最多迴圈兩次執行shouldParkAfterFailedAcquire(),然後陷入阻塞,直到前驅節點成為頭節點並釋放鎖將其喚醒,或者執行緒被中斷,丟擲InterruptedException異常。
public class ReentrantLock implements Lock, java.io.Serializable { //... private final Sync sync; //... public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } //.. } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { //... public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } //... private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } } //... private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } //... }
最後,筆者就簡單介紹下公平鎖(FairSync)的tryAcquire(int acquires)方法,如果我們檢視ReentrantLock的原始碼會發現,在執行lock()、tryLock(long timeout, TimeUnit unit)和lockInterruptibly(),這幾個方法最終都會呼叫到AQS對應的三個方法:acquire(int arg)、tryAcquireNanos(int arg, long nanosTimeout)、acquireInterruptibly(int arg),這三個方法在搶鎖的時候會優先執行子類實現的tryAcquire(int arg)方法,也就是公平鎖(FairSync)或者非公平鎖(NonfairSync)實現的tryAcquire(int arg)方法,搶鎖失敗再執行AQS自身實現的入隊、阻塞方法。
下面,我們來看下公平鎖實現的tryAcquire(int acquires)方法,其實這個方法的實現也非常簡單,先判斷鎖目前是否處於無主狀態,是的話再判斷佇列中是否有等待執行緒,確認鎖是無主狀態且佇列中沒有等待執行緒,便開始嘗試搶鎖,搶鎖成功則直接返回。公平鎖相較於非公平鎖的搶鎖邏輯,也僅僅是多了一步而已,判斷鎖是否無主,是的話再判斷佇列中是否有等待執行緒,不像非公平鎖,只要判斷是無主執行緒便不再檢視等待佇列,直接嘗試搶鎖。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ @ReservedStackAccess protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果state為0,且佇列中沒有等待執行緒,則嘗試搶鎖 if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }