AbstractQueuedSynchronizer原理剖析

爬蜥發表於2019-01-19

佇列同步器AbstractQueuedSynchronizer(簡稱同步器),主要是用於構建鎖或其他同步元件(例如Semaphore)的基礎框架,它使用了一個int成員變數表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作,成為實現大部分同步需求的基礎。《Java併發程式設計的藝術》

上一篇介紹ReentrantLock可重入鎖時提到其底層實現為同步器,其內部定義一個靜態內部類繼承AbstractQueuedSynchronizer,通過實現相關模版方法來完成執行緒同步鎖的功能。

基礎知識

同步器提供了3個方法(getState()、setState(int newState)、compareAndSetState(int expect, int update))來實現狀態的獲取的設定,上述方法通過硬體層面的原子操作保證其安全性。同時,同步器還提供了一系列的模版方法(例如:tryAcquire(int arg)、tryRelease(int arg)),其本身不具體實現。同步元件(鎖、semaphore等)內部通過靜態內部類繼承同步器,實現相關的同步介面,完成執行緒同步功能。鎖是面向使用者的,其定義一系列介面,底層實現交給同步器完成。同步器所提供的模板方法可分為3類:獨佔式獲取和釋放同步狀態、共享式獲取和釋放同步狀態、查詢同步佇列的等待情況。

實現分析

1.同步佇列

同步器內部通過一個同步佇列(FIFO雙向佇列)來完成同步狀態的管理,當執行緒獲取同步狀態失敗時,會將執行緒和等待狀態資訊構造成一個Node物件,並加入到同步佇列,同時會將當前執行緒阻塞。當同步狀態釋放時,會將頭節點的後繼節點執行緒喚醒,並修改頭節點引用。


3344200-489bf28a4a88f077.png
同步佇列的基本結構

上圖中同步器包含兩個節點型別的引用,一個指向頭節點,一個指向尾節點。頭節點即為當前獲取同步狀態成功的節點,頭節點執行緒釋放同步狀態後,會喚醒後繼節點執行緒,當後繼節點執行緒獲取同步狀態成功後會將當前執行緒對應的節點設定為頭節點。因此設定頭節點的操作只存在與當前獲取同步狀態成功的執行緒,不存在併發操作,不需要使用CAS操作來保證其安全性。不同的是,尾節點的設定需要使用CAS操作,原因就是多執行緒併發獲取同步狀態,要保證等待執行緒加入佇列的安全性,只有
compareAndSetTail方法設定成功後,節點才回進入佇列。

2.獨佔式同步狀態獲取與釋放
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

呼叫同步器的acquire(int arg)方法可以獲取同步狀態,該方法不處理執行緒中斷操作,當執行緒獲取同步狀態失敗後加入同步佇列後,對執行緒進行中斷無法從佇列中移除,只有通過前繼節點的喚醒操作,直到獲取同步狀態。執行緒獲取同步狀態失敗後,呼叫addWaiter(Node mode),將執行緒和等待資訊構造的節點加入到同步佇列中,enq(final Node node)方法是一個死迴圈,直到節點節點成功加入同步佇列才會退出。當節點加入同步佇列後呼叫acquireQueued(final Node node, int arg) 方法,該方法內部為一個自旋的死迴圈,會阻塞該執行緒,直到其前驅節點釋放同步狀態,喚醒該執行緒,且成功獲取同步狀態後才會返回。


3344200-7c2df28d07f4e86f.png
獨佔式同步狀態獲取流程
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

執行緒獲取到同步狀態並執行完成後,通過release方法釋放同步狀態,釋放成功後將喚醒頭節點的後繼節點執行緒。

3.共享式同步狀態獲取與釋放

共享式與同步式最大的區別在於同一時刻能否允許多個執行緒同時獲取同步狀態。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    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 (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

共享式同步狀態獲取通過acquireShared方法,doAcquireShared方法實現為自旋操作,直到tryAcquireShared方法返回值大於0,表示成功獲取到同步狀態並退出自旋。共享式同步狀態釋放由於可能存在多個執行緒同時擁有同步狀態,因此其在釋放時必須保證執行緒安全,因此在doReleaseShared方法中會同過自旋和CAS操作實現釋放。

4.獨佔式超時獲取同步狀態

通過同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以實現超時獲取同步狀態,該方法與獨佔式最大的區別在於獲取同步狀態失敗後通過一個超時時間進行自旋,在設定的超時時間內,若成功獲取同步狀態則成功返回,即使其前面有其他等待的執行緒,否則失敗。除此之外,其流程基本上與獨佔式獲取基本相同。

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);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) {
                cancelAcquire(node);
                return false;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}
3344200-b33f03fb096938f2.png
獨佔式超時獲取同步狀態

結束語

本文介紹AbstractQueuedSynchronizer佇列同步器的相關知識,包括使用和實現原理。通過了解同步器的實現,基本上就掌握了鎖的實現原理。因此,瞭解同步器的的實現對於理解鎖的實現機制非常重要。本篇文章內容大部分來自《Java併發程式設計藝術》,有興趣的可以讀下該書,好好看看原始碼。

相關文章