1. 程式人生 > >JDK鎖的基礎--AQS實現原理(三)

JDK鎖的基礎--AQS實現原理(三)

本文主要來分析一下AQS共享模式鎖的獲取和釋放,AQS其實只是一個框架,它主要提供了一個int型別的state欄位,子類繼承時用於儲存子類的狀態,並且提供了一個等待佇列以及維護等待佇列的方法。至於如何使用這個狀態值和等待佇列,就需要子類根據自己的需求來實現了。

以Semaphore類為例,Semaphore允許多個執行緒同時獲得訊號量先來看一下Semaphore的介面:

    //Semaphore
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1
); }

同樣的,sync是一個定義在Semaphore中的AQS的抽象子類,在Semaphore類中有兩種實現,一個是公平的,一個是非公平的。轉到AQS中的acquireSharedInterruptibly方法,

    //AbstractQueuedSynchornizer
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new
InterruptedException(); //由於本文分析共享模式鎖,所以說tryAcquireShared嘗試獲取的是permit而不是鎖 //tryAcquireShared嘗試獲取相應數量的permit,如果失敗返回負值。返回0代表獲取成功但是下次呼叫會失敗,返回正值代表獲取成功而且下次呼叫可能也會成功 //可以理解為返回0代表只有0個permit,所以下次呼叫會失敗,而返回正值代表還有permit,所以下次呼叫可能會成功 if (tryAcquireShared(arg) < 0) //獲取失敗後需要新建一個等待節點並將節點加入等待佇列
doAcquireSharedInterruptibly(arg); } private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //新建一個共享模式的節點並將其加入等待佇列 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { //如果其前驅節點是頭節點,那麼再次嘗試獲取permit int r = tryAcquireShared(arg); if (r >= 0) { //如果獲取成功那麼將該節點設定成頭節點,並且如果r>0,代表還有剩餘的permit,所以如果該節點的後繼節點也是共享模式的,就把後繼節點也喚醒 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

來看一下setHeadAndPropagate方法,這個方法和setHead不同的地方在於它不僅設定了等待佇列的頭節點,並且檢查其後繼節點是否可能是共享模式節點,如果是,而且傳入的propagate大於0或者頭節點設定了PROPAGATE狀態,那麼需要呼叫doReleaseShared方法來喚醒後繼節點。setHeadAndPropagate方法的處理過程比較保守,可能會導致很多不必要的喚醒。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        //如果propagate>0,代表有剩餘的permit,喚醒共享模式節點
        //如果h.waitStatus = PROPAGATE,表示之前的某次呼叫暗示了permit有剩餘,所以需要喚醒共享模式節點
        //由於PROPAGATE狀態可能轉化為SIGNAL狀態,所以直接使用h.waitStatus < 0來判斷
        //如果現在的頭節點的waitStatus<0,喚醒
        //如果現在的頭節點等於null,喚醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果後繼節點為null,whatever喚醒
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

可以看到setHeadAndPropagate方法的原則是寧濫勿缺,反正doReleaseShared方法會繼續後來的處理:

private void doReleaseShared() {      
        for (;;) {
            Node h = head;
            //如果頭節點不為空且頭節點不等於尾節點,亦即等待佇列中有執行緒在等待
            //需要注意的是,等待佇列的頭節點是已經獲得了鎖的執行緒,所以如果等待佇列中只有一個節點,那就說明沒有執行緒阻塞在這個等待佇列上
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    //如果頭節點的狀態是SIGNAL,代表需要喚醒後面的執行緒(SIGNAL狀態可以看做是後繼節點處於被阻塞中)
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //喚醒後繼節點
                    unparkSuccessor(h);
                }
                //如果頭節點的狀態為0,說明後繼節點還沒有被阻塞,不需要立即喚醒
                //把頭節點的狀態設定成PROPAGATE,下次呼叫setHeadAndPropagate的時候前任頭節點的狀態就會是PROPAGATE,就會繼續呼叫doReleaseShared方法把喚醒“傳播”下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果頭節點被修改了那麼繼續迴圈下去
            if (h == head)                   // loop if head changed
                break;
        }
    }

根據自己的思考總結一下,不保證正確性:

  1. AQS的等待佇列的頭節點在初始化的時候是個啞節點,其它時候代表已經獲取鎖的節點(獨佔模式)或者獲取了permit的節點(共享模式),設定了頭節點的執行緒已經可以執行臨界區程式碼了。也就是說,在共享模式下,獲得了permit的執行緒代表的節點可能被其它節點擠出等待佇列。總之,等待佇列從第二個節點開始才是正在等待的執行緒。
  2. AQS的等待佇列的節點類Node只有在其後繼節點被阻塞的情況下才會是SIGNAL狀態,所以SIGNAL狀態代表其後繼節點正在阻塞中。
  3. AQS等待佇列節點的PROPAGATE狀態代表喚醒的行為需要傳播下去,當頭節點的後繼節點並未處於阻塞狀態時(可能是剛呼叫addWaiter方法新增到佇列中還未來得及阻塞),就給頭節點設定這個標記,表示下次呼叫setHeadAndPropagate函式時會把這個喚醒行為傳遞下去。
  4. 設定PROPAGATE狀態的意義主要在於,每次釋放permit都會呼叫doReleaseShared函式,而該函式每次只喚醒等待佇列的第一個等待節點。所以在本次歸還的permit足夠多的情況下,如果僅僅依靠釋放鎖之後的一次doReleaseShared函式呼叫,可能會導致明明有permit但是有些執行緒仍然阻塞的情況。所以在每個執行緒獲取到permit之後,會根據剩餘的permit來決定是否把喚醒傳播下去。但不保證被喚醒的執行緒一定能獲得permit。
  5. 共享模式下會導致很多次不必要的喚醒。