前言
繼續JUC包中ReentrantReadWriteLock的學習,今天學習釋放鎖。
一、寫鎖釋放鎖
入口方法
1 public void unlock() { 2 sync.release(1); 3 }
進入AQS追蹤release方法:
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) { 3 Node h = head; 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h); 6 return true; 7 } 8 return false; 9 }
可見跟ReentrantLock調用的同一個釋放鎖方法,不同點就是tryRelease方法,所以此處只看此方法即可。讀寫鎖tryRelease方法的實現在其內部類Sync中封裝,如下所示:
1 protected final boolean tryRelease(int releases) { 2 if (!isHeldExclusively()) // 判斷當前線程是不是記錄的獨占線程,不是的話不能釋放 3 throw new IllegalMonitorStateException(); 4 int nextc = getState() - releases; 5 boolean free = exclusiveCount(nextc) == 0; // 判斷減完之后是不是0,是0的話說明當前線程都釋放了,將獨占線程設置為空,后面排隊的可以搶占鎖了 6 if (free) 7 setExclusiveOwnerThread(null); 8 setState(nextc); 9 return free; 10 }
跟ReentrantLock中唯一不同的地方是對於free的賦值,因為寫鎖的重入次數是記錄在state的低16位上,所以此處要獲取一下,其余的邏輯都一樣。
二、讀鎖釋放鎖
1 public void unlock() { 2 sync.releaseShared(1); 3 }
進入AQS追蹤releaseShared方法:
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) { 3 doReleaseShared(); 4 return true; 5 } 6 return false; 7 }
只有兩個關鍵方法tryReleaseShared和doReleaseShared,下面分別看看它們的實現邏輯。
1、tryReleaseShared
1 protected final boolean tryReleaseShared(int unused) { 2 Thread current = Thread.currentThread(); 3 if (firstReader == current) {// 如果當前線程是第一個獲取鎖的,因為有兩個成員變量直接記錄,所以只要修改這兩個成員變量的值即可 4 // assert firstReaderHoldCount > 0; 5 if (firstReaderHoldCount == 1) // count為1,此時不應該把它置為0嗎 6 firstReader = null; 7 else 8 firstReaderHoldCount--; 9 } else { // 不是當前線程,則要去緩存獲取或者本地線程變量中獲取當前線程的重入次數,給它減一,如果次數小於等於1則直接移除 10 HoldCounter rh = cachedHoldCounter; 11 if (rh == null || rh.tid != getThreadId(current)) 12 rh = readHolds.get(); 13 int count = rh.count; 14 if (count <= 1) { 15 readHolds.remove(); 16 if (count <= 0) 17 throw unmatchedUnlockException(); 18 } 19 --rh.count; 20 } // 維護state的鎖重入次數/獲取次數記錄 21 for (;;) { 22 int c = getState(); 23 // 因為是讀鎖,所以一個SHARED_UNIT的值代表一個鎖 24 int nextc = c - SHARED_UNIT; 25 if (compareAndSetState(c, nextc)) 26 // 如果只有讀鎖,這里返回什么都無所謂;所以此返回值是專門為寫鎖准備的,后續會根據返回值去喚醒寫鎖, 27 return nextc == 0; 28 } 29 }
該方法邏輯很清晰,for循環上面的部分代碼,用戶將讀鎖當前線程記錄的重入次數-1;for循環用於將AQS中維護的state中的讀鎖占有次數-1.返回的布爾類型用於給后續方法判斷是否要喚醒寫鎖。后續方法即我們下一步要追蹤的doReleaseShared方法。
2、doReleaseShared
1 private void doReleaseShared() { 2 // 喚醒后續的寫線程 3 for (;;) { 4 Node h = head; 5 if (h != null && h != tail) { 6 int ws = h.waitStatus; 7 if (ws == Node.SIGNAL) { // 第一種:如果狀態是-1,說明后面肯定有阻塞的任務,要去喚醒它 8 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 9 continue; // loop to recheck cases 10 unparkSuccessor(h); 11 } // ws等於0說明是最后一個節點了,此時將Node的ws設置為PROPAGATE,因為后續沒有節點了,所以不用喚醒 12 else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 13 continue; // loop on failed CAS 14 } 15 if (h == head) // 不等於head說明又有新的讀鎖進來了,這時要繼續循環 16 break; 17 } 18 }
此方法用於喚醒讀鎖后處於掛起狀態的鎖,讀鎖后處於掛起狀態的鎖有兩種:第一種是寫鎖,這很好理解,如果有讀鎖被占用,寫鎖過來的時候肯定需要掛起等讀鎖執行完(非相同線程),讀鎖執行完之后喚醒這個寫鎖;第二種是讀鎖,為什么當前執行的是讀鎖而后面還會有讀鎖被掛起呢?這就要回到系列(三)中的內容了,在系列(三)讀鎖加鎖中我們講過一個 apparentlyFirstQueuedIsExclusive 方法,該方法會判斷隊列中第一個排隊的是不是寫鎖,如果是寫鎖則讓當前的讀鎖掛起不去競爭鎖,而若在隊首寫鎖等待的過程中有多個讀鎖過來,則這多個讀鎖都會被依次掛起,這時就會出現第二種情況,即讀鎖執行的時候后面還有一個讀鎖被掛起,執行完之后需喚醒它。
此處第二種讀鎖喚醒讀鎖的場景,是在讀鎖加鎖時觸發的。在系列(三)中對這里未涉及,現在我們再回頭看看。在doAcquireShared方法中,有個setHeadAndPropagate方法,在該方法中會檢測下一個節點是不是讀鎖,如果是就調用doReleaseShared方法喚醒它。
小結
讀寫鎖的釋放鎖邏輯基本就這些了,下面再做一個小結。
寫鎖釋放邏輯跟ReentrantLock中的釋放鎖邏輯基本一致,因為畢竟都是獨占鎖。
讀鎖釋放則復雜的多,它會先釋放每個讀鎖線程記錄的重入次數,再去減掉state中記錄的加鎖次數,最后還要喚醒后面掛起的線程。喚醒掛起的線程又分兩種情況,一種是喚醒后面的寫鎖線程,另一種是喚醒讀鎖線程。讀鎖之間不互斥為什么在讀鎖執行時還會有讀鎖被掛起?是因為在讀鎖加鎖時為防止寫鎖飢餓如果判斷隊首有寫鎖在等待獲取鎖那么后來的讀鎖都要掛起等待,這時就會出現多個讀鎖被掛起的情況。在釋放讀鎖時喚醒的線程是寫鎖線程,在讀鎖加鎖時喚醒的線程是讀鎖線程。
另外,對於Node.PROPAGATE這個狀態一直沒看出它的作用,而且查看了一下使用的地方,發現只在上面的doReleaseShared方法中用過,所以個人覺得是個可有可無的狀態,不知道是為后續擴展留的狀態還是有其他作用我沒看出來,如果有對此有理解的園友,歡迎給答疑解惑一下,感謝!