高併發之JUC——AQS源碼深度分析,有你不得而知的條件等待隊列(二)

上一篇分析了關於AQS獨佔鎖的執行流程和源碼。在AQS中不僅涉及了獨佔鎖,還涉及了共享鎖及帶超時時間的共享鎖、中斷共享鎖。本文就講解上述鎖的獲取鎖和釋放鎖的原理。

AQS獲取共享鎖acquireShared

共享鎖,顧名思義就是多個線程可以共享同一把鎖,在JUC下面如CountDownLatch就是基於共享鎖實現的。

那麼瞭解了共享鎖是什麼,那麼先來看下它是如何獲取鎖的。

源碼

共享鎖獲取鎖和獨佔鎖獲取鎖在AQS中的邏輯是基本一致的,流程圖可以參考我上一篇中關於AQS獨佔鎖加鎖的流程圖。接下來先看下執行代碼:

acquireShared:

public final void acquireShared(int arg) {
    // 獲取鎖方法由各自子類實現
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

下面的方法和獨佔鎖一樣,在上一篇中已經對代碼有詳細說明,唯一與獨佔鎖處理邏輯不一樣的地方在於setHeadAndPropagate方法,獨佔鎖此處只是將獲取到鎖的節點設置爲了頭節點,但是共享鎖不僅將獲取到鎖的節點設置爲了頭節點,還會喚醒下一個節點。那麼我們重點看下setHeadAndPropagate這個方法。

doAcquireShared:

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) {
                // 競爭鎖,當返回值<0的時候,那麼就是獲取鎖失敗,反之則成功獲取鎖。
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 這塊是和獨佔鎖不一致的地方。獨佔鎖只是將獲得鎖的節點直接設置爲頭節點,這裏不僅設置爲頭節點,還向下傳播。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate:這個方法首先將獲取到鎖的節點設置爲頭節點,然後喚醒下一個節點。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        // 將當前獲取到鎖的節點設置爲頭節點
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        // 滿足向下傳播條件
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 獲取到鎖的節點的next節點是共享鎖節點,那麼喚醒後續節點
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

doReleaseShared:喚醒下一個節點,之後這個節點就有機會去競爭鎖了。

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
                    // 釋放h.next節點
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
實現原理

1、線程A請求鎖,首先去獲取鎖。若成功獲取到鎖,直接返回,否則執行doAcquireShared。假設當前沒有線程持有這把鎖,且線程A成功獲取到這把鎖,運行線程A。

2、此時線程B請求加鎖,同樣也會嘗試獲取鎖,那麼正常此時獲取鎖失敗(A線程持有了這把鎖)。執行doAcquireShared。

3、doAcquireShared是一個死循環,在doAcquireShared中將請求B加入等待隊列,在加入前,等待隊列爲空,加入後,等待隊列如圖1所示。

4、此時B線程會判斷在等待隊列中B線程的前繼節點是不是head,如果是,則在此獲取鎖。目前的情況假設A還沒有釋放線程,B依然拿不到鎖。那麼進入到下一次循環,因爲doAcquireShared是一個死循環,拿不到鎖就一直阻塞在這個方法中。

5、此時假設A還未釋放鎖,那麼當線程C請求過來的時候,同樣會經歷步驟2和3,然後依然會判斷線程C對應的節點的前繼節點是不是頭結點,此時肯定不是頭結點,C的前繼節點是B節點,那麼會執行shouldParkAfterFailedAcquire方法。

6、在shouldParkAfterFailedAcquire方法中,首先會判斷C節點的前繼節點(也就是B節點)的狀態是不是SIGNAL(表示存在後繼節點需要被喚醒),此時節點B的狀態爲0(初始化狀態),當狀態爲0是,會將B節點的狀態置爲SIGNAL,並且返回false。

7、此時C線程請求返回到doAcquireShared方法,因爲返回了false,所以根據doAcquireShared方法的邏輯,會進入下一次循環(因爲是死循環)。此時C執行下一次循環後,又會執行到shouldParkAfterFailedAcquire方法(因爲C節點的前繼節點還不是head),但此時執行shouldParkAfterFailedAcquire方法會返回true(因爲前繼節點B的狀態在上面已經設置爲SIGNAL)了。返回true後,在doAcquireShared方法中會執行parkAndCheckInterrupt方法,阻塞當前線程,也就是線程C被阻塞掛起了,等待其他線程來解阻塞。

綜上,此時線程A持有鎖,線程B在自旋,狀態爲SIGNAL,並且一直嘗試獲取鎖,線程C阻塞掛起,狀態爲0。

AQS釋放共享鎖

AQS釋放共享鎖和上一篇講的獨佔鎖處理是基本一致的,都是喚醒後續節點。

1、假設此時A線程執行結束,釋放鎖。當A釋放鎖成功後,會執行doReleaseShared方法。
2 、在doReleaseShared中,主要是釋放下一個節點,讓它有機會去競爭鎖,這裏也就是喚醒B線程。

AQS可打斷共享鎖

acquireSharedInterruptibly:這個方法與共享鎖獲取鎖邏輯基本一致,唯一不同的是在嘗試獲取鎖之前,會判斷當前線程的打斷狀態,如果當前線程線程被打斷,那麼直接拋出異常。

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    // 判斷當前線程是否被打斷,如果打斷,那麼直接拋出異常
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

釋放鎖的流程和共享鎖是一致的。

AQS超時模式獨佔鎖

在獲取鎖的時候可以傳入一個超時時間,當自旋等待的時間超過傳入的等待時間時,那麼返回獲取鎖失敗,然後直接取消該線程所屬節點(也就是將在等待隊列中的該節點的狀態設置爲canceled)。

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 會判斷等待是否超時,如果超時就直接返回,不會在這裏自旋等待。
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            // 如果獲取鎖失敗,可能是等待超時,那麼直接將該節點設置爲取消狀態。
            if (failed)
                cancelAcquire(node);
        }
    }

重點看下上面帶註釋的兩行代碼,分別爲判斷等待時間是否超過傳入的等待時間,也就是等待是否超時,如果等待超時,那麼直接返回獲取鎖失敗,並取消該節點。其餘的處理和獨佔鎖獲取鎖流程是完全一致的。

條件等待隊列

我們先看下面的代碼實例:


    static ReentrantLock reentrantLock = new ReentrantLock();
    static Condition condition = reentrantLock.newCondition();

    static int a = 0;

    public static void add() {
        reentrantLock.lock();

        while (a == 0) {
            try {
                a++;
                System.out.println("add, a="+a);
                condition.signal();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        reentrantLock.unlock();
    }

    public static void sub() {
        reentrantLock.lock();

        while (a == 1) {
            try {
                a--;
                System.out.println("sub, a="+a);
                condition.signal();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        reentrantLock.unlock();
    }
    
    public static void main(String[] args) throws Exception{

        new Thread(new Runnable() {
            @Override
            public void run() {
                add();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                sub();
            }
        }).start();

    }

上面的代碼很簡單,是個簡答的生產者消費者模型。其中ReentrantLock是獨佔鎖,這個我們後面會有單獨的文章來講解。下面想說的是reentrantLock.newCondition(),那麼Condition是什麼呢?下面我們就來說一下。

Condition是AQS中的另一個隊列,之前我們說過的隊列是AQS等待隊列,現在我們要說的也是AQS類中的一個隊列,叫條件等待隊列。首先當前線程要先獲取到鎖,當滿足指定條件(程序員自己指定)時,可通過signal來喚醒其他線程來獲取鎖,然後通過await方法將當前線程阻塞,這個就是條件隊列的作用,也就是可以切換不同線程來獲取同一把鎖。在AQS的源碼中定義如下:

public class ConditionObject implements Condition, java.io.Serializable {

        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        // 頭節點
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        // 尾節點
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }

        // Internal methods

        /**
         * Adds a new waiter to wait queue.
         * @return its new wait node
         */
         // 向隊列中加入節點,狀態爲CONDITION
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

        /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

        /**
         * Removes and transfers all nodes.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

        /**
         * Moves the longest-waiting thread, if one exists, from the
         * wait queue for this condition to the wait queue for the
         * owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        ... ...
    }

上面代碼就是在AQS源碼中定義的條件等待隊列,包含了頭節點和尾節點兩個Node,這裏其實就是一個雙向鏈表。至於signal和await方法的原理,我將在後面的文章中專門來詳細講解,這裏只需知道在AQS中不僅定義了一個等待隊列,還定義了一個條件等待隊列來供子類實現更多的功能(喚醒其他線程,阻塞當前線程)。

最後

本文包括上一篇AQS文章基本把AQS的總體結構及源碼都介紹了一下,大家應該對AQS有了一個總體印象。接下來的本系列文章將分析JUC包下面的具體實現類,如ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等。

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