讀寫鎖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()方法會以一種風暴的形式喚醒后續的第一個獲取寫鎖之前的所有獲取讀鎖的節點,沒有寫鎖將會喚醒整個隊列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM