讀寫鎖doReleaseShared源碼分析及喚醒後繼節點的過程分析

文章結構

  • 源碼:對doReleaseShared()方法的源碼進行一些注釋
  • 使用場景:介紹doReleaseShared()使用位置,及目的
  • 以寫鎖開始的隊列:分析寫鎖開始得同步等待隊列在喚醒後續讀鎖節點的過程
  • 以讀鎖開始的隊列
  • 總結

源碼

這個方法在AQS中實現,具體解析見注釋

/**
 * Release action for shared mode -- signals successor and ensures
 * propagation. (Note: For exclusive mode, release just amounts
 * to calling unparkSuccessor of head if it needs signal.)
 */
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            //在隊列中的節點對應的執行緒阻塞之前,將前驅節點的waitStatus狀態設置為SIGNAL
            //所以這塊ws==0,其實是當前執行緒通過第一次循環將狀態設置為了0,
            //第二次循環進入的時候頭節點還沒有被改變
            //cas操作失敗的話會直接continue,為什麼會失敗,
            //可能是喚醒得其他節點在喚醒後續節點的時候已經進行了修改
            //修改失敗則代表頭節點已經修改,則進入下一次循環
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //特別注意這個出口判斷
        //喚醒後繼節點之後,後繼節點沒有更換頭節點才會退出,整個後繼節點可以是一個讀鎖,或者寫鎖
        //在喚醒到隊列尾之後頭節點將不再改變,可以結束
        if (h == head)                   // loop if head changed
            break;
    }
}

使用場景

doReleaseShared()的作用喚醒其後後繼節點,具體的說是需要喚醒其後到下一個嘗試獲取鎖的的節點之間的所有嘗試獲取
讀鎖的執行緒。

在AQS中一共有兩處使用到了doReleaseShared()方法,分別是:

  • 在setHeadAndPropagate()中,setHeadAndPropagate()方法用於同步等待隊列中獲取共享鎖的節點
    在成功獲取共享鎖之後判斷其是否有後繼節點,以及後繼節點是否是嘗試獲取共享鎖,如果是則調用doReleaseShared()完成喚醒操作

  • 在releaseShared()中當前執行緒釋放完讀鎖後,讀鎖歸零則調用doReleaseShared()方法喚醒後及執行緒

總之來說,doReleaseShared()就是用來喚醒後繼節點的,但是這個方法體式一個死循環,而出口條件卻不是很好理解;

//方法出口
if (h == head)                   // loop if head changed
    break;

如何能滿足這個條件呢,以讀鎖為例說明:

以寫鎖開始的隊列

假設當前讀鎖被執行緒A獲取,考慮獲取讀鎖的進入隊列的條件,非公平模式下隊列中頭結點的後繼節點嘗試獲取寫鎖,則會加入到隊列中;
公平模式下,隊列中有等候的節點就會加入到隊列中排隊,但是讀鎖是非阻塞式獲取的,當一個執行緒獲取讀鎖後,
其他執行緒也可以獲取讀鎖,CAS操作放在一個死循環中完成,不會被加入到隊列,所以第一個放到隊列中的也是一個寫鎖的獲取執行緒。
若當前是寫鎖被獲取,則統統會被加入到隊列中。

假設有這樣一個隊列(如下圖)

當寫鎖被獲取並剛釋放的瞬間,還沒有喚醒讀鎖1,則隊列變為下面的樣子

此時讀鎖1被阻塞再doAcquireShared方法上,這時喚醒讀鎖1,讀鎖1執行緒獲取讀鎖成功後會調用setHeadAndPropagate()方法
,判斷出其後面還有等待的執行緒讀鎖2則調用doReleaseShared()方法。現在再來看doReleaseShared()方法,
這裡分為兩種情況:

在讀鎖1判斷頭節點之前,讀鎖2執行緒替換頭節點成功

讀鎖1將自身的waitStatus欄位設置為0(compareAndSetWaitStatus(h, Node.SIGNAL, 0設置失敗則循環設置),
並喚醒讀鎖2之後,讀鎖2立刻加鎖成功,會將頭節點設置為自身節點(thread欄位置空,如下圖),讀鎖1的h會與頭節點不同

那麼讀鎖1執行緒會在這個循環里不能退出,第二次循環的時候h欄位會變成曾經的讀鎖2執行緒對應的節點,

  • 讀鎖2執行緒此時是被喚醒的,讀鎖2執行緒也會調用setHeadAndPropagate()方法去喚醒讀鎖3執行緒。
    假設是讀鎖2執行緒喚醒了讀鎖3,讀鎖3執行緒會將頭節點設置為自身節點,而讀鎖1執行緒的h欄位保存的頭節點還沒更改依然是
    讀鎖執行緒2的情況下,CAS更改頭節點的waitStatus狀態操作將會失敗,會進入到else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))當中執行下一次循環,還是不能結束。

  • 由於讀鎖1在循環,所以有可能是讀鎖1喚醒了讀鎖3,讀鎖2對應的執行緒CAS更改頭節點的waitStatus狀態操作將會失敗,
    會進入到else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))當中執行下一次循環,還是不能結束。

  • 假設讀鎖3對應的執行緒由讀鎖2喚醒,讀鎖三完成了設置頭節點的操作,此時讀鎖1剛好進行一次循環,並且沒有競爭,那麼讀鎖1可以立刻喚醒讀鎖4

假設隊列長度足夠,那麼就會產生一個喚醒的風暴,前面的執行緒都在喚醒後面的執行緒,這樣可以快速的喚醒起隊列中下一個寫鎖之前的所有申請讀鎖的執行緒。
這樣的風暴會在碰到一個申請寫鎖的執行緒或者一直到隊列尾都沒有寫鎖,喚醒了所有的執行緒之後結束,當然中間可能存在部分的執行緒已經停止了喚醒操作(
在判斷h==head完成之前,頭節點沒有被替換)

  • 碰到寫鎖:由於讀鎖已經被獲取,喚醒一個寫鎖執行緒後,並不能完成加鎖操作,因此頭節點不會被替換,直到所有的讀鎖被釋放,寫鎖才能嘗試加鎖
    所以在這個位置將會結束這場風暴。

  • 到達隊尾:到達隊尾後頭節點將不會變化,風暴結束

在讀鎖1判斷頭節點完成之前,讀鎖2執行緒都沒有替換頭節點

讀鎖1喚醒讀鎖2對應的執行緒,但是讀鎖2處於某些原因並沒有立刻加鎖成功,或者加鎖成功但是換么有用自身節點將頭節點替換,此時if (h == head)
將被滿足,從而讀鎖1執行緒退出,後面的執行緒依然會被喚醒,因為讀鎖2執行緒已經被喚醒,可以繼續後面的喚醒操作

以讀鎖開始的隊列

就是以寫鎖開始得隊列得寫鎖執行完成後得喚醒過程,(當前鎖狀態中讀鎖被獲取,且隊列的頭節點得後繼節點不存在寫鎖申請,不知道那種情況讀鎖會入隊列)

總結

doReleaseShared()方法會以一種風暴的形式喚醒後續的第一個獲取寫鎖之前的所有獲取讀鎖的節點,沒有寫鎖將會喚醒整個隊列