深刻淺出AQS之共享鎖模式

搞清楚AQS獨佔鎖的實現原理以後,再看共享鎖的實現原理就會輕鬆不少。兩種鎖模式之間不少通用的地方本文只會簡單說明一下,就不在贅述了node

1、執行過程概述

獲取鎖的過程:面試

  1. 當線程調用acquireShared()申請獲取鎖資源時,若是成功,則進入臨界區。
  2. 當獲取鎖失敗時,則建立一個共享類型的節點並進入一個FIFO等待隊列,而後被掛起等待喚醒。
  3. 當隊列中的等待線程被喚醒之後就從新嘗試獲取鎖資源,若是成功則喚醒後面還在等待的共享節點並把該喚醒事件傳遞下去,即會依次喚醒在該節點後面的全部共享節點,而後進入臨界區,不然繼續掛起等待。

釋放鎖過程:設計模式

  1. 當線程調用releaseShared()進行鎖資源釋放時,若是釋放成功,則喚醒隊列中等待的節點,若是有的話。

2、源碼深刻分析

基於上面所說的共享鎖執行流程,咱們接下來看下源碼實現邏輯:
首先來看下獲取鎖的方法acquireShared(),以下併發

public final void acquireShared(int arg) {
        //嘗試獲取共享鎖,返回值小於0表示獲取失敗
        if (tryAcquireShared(arg) < 0)
            //執行獲取鎖失敗之後的方法
            doAcquireShared(arg);
    }

這裏tryAcquireShared()方法是留給用戶去實現具體的獲取鎖邏輯的。關於該方法的實現有兩點須要特別說明:學習

1、該方法必須本身檢查當前上下文是否支持獲取共享鎖,若是支持再進行獲取。ui

2、該方法返回值是個重點。其1、由上面的源碼片斷能夠看出返回值小於0表示獲取鎖失敗,須要進入等待隊列。其2、若是返回值等於0表示當前線程獲取共享鎖成功,但它後續的線程是沒法繼續獲取的,也就是不須要把它後面等待的節點喚醒。最後、若是返回值大於0,表示當前線程獲取共享鎖成功且它後續等待的節點也有可能繼續獲取共享鎖成功,也就是說此時須要把後續節點喚醒讓它們去嘗試獲取共享鎖。線程

有了上面的約定,咱們再來看下doAcquireShared方法的實現:設計

//參數很少說,就是傳給acquireShared()的參數
    private void doAcquireShared(int arg) {
        //添加等待節點的方法跟獨佔鎖同樣,惟一區別就是節點類型變爲了共享型,再也不贅述
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //表示前面的節點已經獲取到鎖,本身會嘗試獲取鎖
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //注意上面說的, 等於0表示不用喚醒後繼節點,大於0須要
                    if (r >= 0) {
                        //這裏是重點,獲取到鎖之後的喚醒操做,後面詳細說
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //若是是由於中斷醒來則設置中斷標記位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //掛起邏輯跟獨佔鎖同樣,再也不贅述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //獲取失敗的取消邏輯跟獨佔鎖同樣,再也不贅述
            if (failed)
                cancelAcquire(node);
        }
    }

獨佔鎖模式獲取成功之後設置頭結點而後返回中斷狀態,結束流程。而共享鎖模式獲取成功之後,調用了setHeadAndPropagate方法,從方法名就能夠看出除了設置新的頭結點之外還有一個傳遞動做,一塊兒看下代碼:code

//兩個入參,一個是當前成功獲取共享鎖的節點,一個就是tryAcquireShared方法的返回值,注意上面說的,它可能大於0也可能等於0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //記錄當前頭節點
        //設置新的頭節點,即把當前獲取到鎖的節點設置爲頭節點
        //注:這裏是獲取到鎖以後的操做,不須要併發控制
        setHead(node);
        //這裏意思有兩種狀況是須要執行喚醒操做
        //1.propagate > 0 表示調用方指明瞭後繼節點須要被喚醒
        //2.頭節點後面的節點須要被喚醒(waitStatus<0),不管是老的頭結點仍是新的頭結點
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //若是當前節點的後繼節點是共享類型或者沒有後繼節點,則進行喚醒
            //這裏能夠理解爲除非明確指明不須要喚醒(後繼等待節點是獨佔類型),不然都要喚醒
            if (s == null || s.isShared())
                //後面詳細說
                doReleaseShared();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

最終的喚醒操做也很複雜,專門拿出來分析一下:
注:這個喚醒操做在releaseShare()方法裏也會調用。隊列

private void doReleaseShared() {
        for (;;) {
            //喚醒操做由頭結點開始,注意這裏的頭節點已是上面新設置的頭結點了
            //其實就是喚醒上面新獲取到共享鎖的節點的後繼節點
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示後繼節點須要被喚醒
                if (ws == Node.SIGNAL) {
                    //這裏須要控制併發,由於入口有setHeadAndPropagate跟release兩個,避免兩次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //執行喚醒操做      
                    unparkSuccessor(h);
                }
                //若是後繼節點暫時不須要喚醒,則把當前節點狀態設置爲PROPAGATE確保之後能夠傳遞下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //若是頭結點沒有發生變化,表示設置完成,退出循環
            //若是頭結點發生變化,好比說其餘線程獲取到了鎖,爲了使本身的喚醒動做能夠傳遞,必須進行重試
            if (h == head)                   
                break;
        }
    }

接下來看下釋放共享鎖的過程:

public final boolean releaseShared(int arg) {
        //嘗試釋放共享鎖
        if (tryReleaseShared(arg)) {
            //喚醒過程,詳情見上面分析
            doReleaseShared();
            return true;
        }
        return false;
    }

注:上面的setHeadAndPropagate()方法表示等待隊列中的線程成功獲取到共享鎖,這時候它須要喚醒它後面的共享節點(若是有),可是當經過releaseShared()方法去釋放一個共享鎖的時候,接下來等待獨佔鎖跟共享鎖的線程均可以被喚醒進行嘗試獲取。

3、總結

跟獨佔鎖相比,共享鎖的主要特徵在於當一個在等待隊列中的共享節點成功獲取到鎖之後(它獲取到的是共享鎖),既然是共享,那它必需要依次喚醒後面全部能夠跟它一塊兒共享當前鎖資源的節點,毫無疑問,這些節點必須也是在等待共享鎖(這是大前提,若是等待的是獨佔鎖,那前面已經有一個共享節點獲取鎖了,它確定是獲取不到的)。當共享鎖被釋放的時候,能夠用讀寫鎖爲例進行思考,當一個讀鎖被釋放,此時不管是讀鎖仍是寫鎖都是能夠競爭資源的。

歡迎關注公衆號 【碼農開花】一塊兒學習成長 我會一直分享Java乾貨,也會分享免費的學習資料課程和麪試寶典 回覆:【計算機】【設計模式】【面試】有驚喜哦

相關文章
相關標籤/搜索