【Java併發編程】AQS(4)——共享鎖的獲取與釋放

 

今天來說下共享鎖的獲取與釋放,建議大家在看這篇文章之前,先將我寫的關於獨佔鎖的文章看一下,其中涉及了許多重複的方法,在這篇文章中就不會再次講解了。好了,我們先來看共享鎖的獲取吧

 

一.  共享鎖的獲取

 

在AQS中共享鎖的獲取一共有三個方法,今天主要講第一個

  1. acquireShared:不響應中斷獲取鎖

  2. acquireSharedInterruptibly:響應中斷獲取鎖

  3. tryAcquireSharedNanos:響應中斷與超時獲取鎖

在讀源碼前,我們需要注意兩點:

  1. 共享鎖模式與獨佔鎖模式最大的區別在於獨佔鎖模式同一時間只能有一個線程持有鎖,臨界區只能被串行訪問;共享鎖模式在同一時間可以有多個線程持有鎖

  2. 在共享鎖模式下,獲取鎖和釋放鎖成功後,都會去喚醒後繼Node;獨佔鎖模式下只在釋放鎖成功後才喚醒後繼Node

開始看acquireShared源碼吧 

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

 

1  tryAcquireShared

 

這個方法是由子類實現的,其功能是用來獲取共享鎖,具體的實現會在後續的文章中說道,但這裏我們需要提前瞭解下返回值的含義(下面源碼只將返回值的註釋保留了下來)

/**
* @return a negative value on failure; zero if acquisition in shared
*         mode succeeded but no subsequent shared-mode acquire can
*         succeed; and a positive value if acquisition in shared
*         mode succeeded and subsequent shared-mode acquires might
*         also succeed, in which case a subsequent waiting thread
*         must check availability. (Support for three different
*         return values enables this method to be used in contexts
*         where acquires only sometimes act exclusively.)  Upon
*         success, this object has been acquired.
*/
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

我們可以看到返回值是int值,我們通過註釋可以知道這個int型返回值的含義

  1. 返回值<0:代表失敗

  2. 返回值=0:代表獲取到鎖但後繼節點獲取鎖是不能成功的

  3. 返回值>0:代表獲取鎖成功且後繼節點獲取鎖可能會成功

瞭解返回值的意義後,我們再回到acquireShared方法,如果tryAcquireShared返回值小於0,說明獲取共享鎖失敗了,因此我們就要執行doAcquireShared

 

2  doAcquireShared

 

在看這個方法之前,再去看一眼"獨佔鎖獲取"中說的acquireQueued方法。。。看完了吧,我們現在來看下doAcquireShared方法 

 

是不是和 acquireQueued很像,其實他們邏輯都是一樣的,且裏面大部分方法都相同,其中的不同點我用紅框標註出來了

我們先看第一個不同點,這個就是"獨佔鎖獲取"中用到的入隊方法,只不過共享鎖模式中把入隊和喚醒操作都放doAcquireShared一個方法裏,這裏就不再贅述了

我們再看第二個。如果此Node的前驅Node是head,則會再次通過tryAcquireShared去獲取共享鎖,如果返回狀態是大於等於0,說明獲取共享鎖成功,就會進入到if分支內,否則就會跳到外面的第二個if分支去執行掛起操作。我們具體看獲取鎖成功後調用的setHeadAndPropagate方法

 

2.1  setHeadAndPropagate

 

這個方法主要完成兩件事,設置頭節點head以及在一定條件下喚醒後繼Node。還記得這節最開始說過,共享鎖模式不僅會在釋放鎖成功後喚醒後繼Node,在獲取鎖成功時也會喚醒後繼Node,所以setHeadAndPropagate方法會在兩個不同的地方被調用,我們具體看代碼


private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

首先我們通過setHead方法,重新設置頭結點,即將當前獲取鎖的thread置空,然後將head指向當前node,這裏需要特別注意:不管是獨佔鎖獲取成功還是共享鎖獲取成功,都會通過setHead方法將head指向自己,換一句話說,head指向的Node就是最新獲取到鎖的Node(只有在獲取鎖成功後才調用setHead更改head)

 

然後我們會在一定條件下來決定是否喚醒繼Node,最終喚醒後繼Node的是通過doReleaseShared方法,因爲鎖的釋放也要調用此方法,所以我們把這個方法放在下一小節一起討論

 

 

二.  共享鎖的釋放

 

AQS中對共享鎖的釋放方法只有一個,就是releaseShared方法,我們直接看源碼


public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

可以看到,就兩個方法,其中第一個方法tryReleaseShared的方法是子類複寫的方法,返回值爲boolean,如果失敗,則直接跳到最後返回false;否則就返回true,然後進入if分支調用doReleaseShared方法來喚醒後繼Node。沒錯這個doReleaseShared方法就是上一節最後沒講的那個方法,也是很重要的一個方法,我們具體來看看吧

 

1  doReleaseShared


private void doReleaseShared() {
    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);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

我們可以看到,doReleaseShared方法裏面又是個自旋for循環,在分析自旋for循環裏面的代碼之前,我們先來想想這裏爲什要自旋?退出自旋的條件是什麼?

我們是共享鎖模式,所以允許多線程同時擁有鎖,當某個線程正在執行doReleaseShared方法的時候,有個新的線程拿到了鎖,並執行了setHead方法,此時頭結點head就發生了變化(這個變化如下圖所示),所以我們就需要重新拿到新的頭結點head的狀態來再去喚醒後面的Node

因此,我們退出的自旋的條件就是在執行doReleaseShared時head是沒有更改的,如果更改,說明當時拿到的head的後繼Node此時已經被喚醒了,所以我們需要通過自旋來重新定位到此時新的head並再次喚醒後繼Node

這裏大家可能會很好奇,什麼時候會出現上面說的head更換的情況?這種情況的發生一共有兩種,我們結合上面的圖來說明,假設圖中Node1和Node2中的線程分別爲t1和t2

  1. t1執行完Node h = head但還未執行if (h == head)時,有持有鎖的線程成功釋放了鎖,並喚醒了Node2中的線程t2,t2拿鎖成功後執行了setHead

  2. t1執行完upparkSuccessor後但還未執行if (h == head)時,喚醒的t2成功拿到了鎖並執行了setHead

當然,當t1執行到if (h == head),如果if條件成立,即h == head,說明在t1執行doReleaseShared方法期間沒有其他線程執行了setHead方法,所以t1可以退出自旋for循環

弄清了上面兩個問題後,後面就輕鬆多了,我們具體看下doReleaseShared方法的代碼吧

進入自旋for循環後,我們會拿到此時的頭結點,然後我們會判斷,只要同步隊列已初始化且有等待的Node,則會進入最外層的if分支,裏面有兩個分支,對應着兩種情況,我們先來看第一個if分支


if (ws == Node.SIGNAL) {
    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
        continue;            // loop to recheck cases
    unparkSuccessor(h);
}

如果h的狀態是SIGNAL,說明其後繼Node是等待被喚醒的,然後我們會先將h的狀態置爲初始值0,因爲這裏可能會存在多個線程併發調用,所以這步會通過CAS操作保證只有一個線程能成功,失敗的就自旋重來。成功後會調用unparkSuccessor喚醒後繼 Node。需要注意,這裏的併發情況只可能是下面兩種

  1. 多個釋放鎖成功後調用doReleaseShared的線程加一個獲取鎖成功後調用doReleaseShared的線程

  2. 多個釋放鎖成功後調用doReleaseShared的線程

獲取鎖的線程爲什麼只有一個?因爲前面說了,每個線程獲取鎖成功後會重置head,不明白的再回到前面看下

我們接着看else if 分支

else if (ws == 0 &&
         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    continue;

什麼時候ws==0?一種是上面if分支中的compareAndSetWaitStatus(h, Node.SIGNAL, 0)執行成功後,但顯然這裏不是這種情況,因爲如果執行了上面的if分支,就不會進入到else if裏面了

還有一種情況是在head初始化時(即同步隊列初始化)。那什麼時候head會初始化呢?在獨佔鎖獲取中我們講過,只有當有Node入隊時,纔會初始化head。假設此時有個Node要入隊,發現head爲null後,就會通過enq方法初始化head,然後再將head置爲SIGNAL。所以從head初始化到被置爲SIGNAL這很短的一段時間內,head的狀態是爲0的。所以此時我們會將head的狀態置爲propagation,告訴其他線程後繼節點可能是需要被喚醒的

好了,共享鎖的獲取與釋放就講到這裏了,本來今天還想早點睡的,結果寫完就到這個時候了,AQS系列還有最後一篇,應該會在這個週末更新完,困死了,晚安

 

(未完)

歡迎大家關注我的公衆號 “程序員進階之路”,裏面記錄了一個非科班程序員的成長之路

                                                         

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章