Java併發——AbstractQueuedSynchronizer(AQS)同步器

午夜12點發表於2018-08-06

簡介

在此之前介紹ReentrantLockReentrantReadWriteLock中都有sync屬性,而sync正是繼承了AQS(AbstractQueuedSynchronizer)同步器。AQS採用模板設計模式,呼叫其模板方法(獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步佇列中的等待執行緒情況),重寫指定方法,我們自身就能利用AQS構造出自定義同步元件。

AQS解析

重要屬性


    //等待佇列的頭節點
    private transient volatile Node head;
    //等待佇列的尾節點
    private transient volatile Node tail;
    //同步狀態
    private volatile int state;
複製程式碼

AQS內部通過head、tail定義了一個FIFO佇列,state表示同步狀態(0表示未有執行緒獲取同步狀態或鎖,大於0表示有執行緒佔有),都通過volatile修飾,保證了記憶體可見性

重要內部類


static final class Node {
    /** 共享模式 */
    static final Node SHARED = new Node();
    /** 獨佔模式 */
    static final Node EXCLUSIVE = null;
    /**因為在同步佇列中等待的執行緒等待超時或者被中斷,需要從同步佇列中取消等待,節點進入該狀態將不會變化 */
    static final int CANCELLED =  1;
    /** 
     * 後繼節點的執行緒處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或者取消 
     * 將會通知後繼節點,使後繼節點的執行緒執行
     */ 
    static final int SIGNAL    = -1;
    /**
     * 節點在等待佇列中,節點執行緒等待在Condtion上,當其他執行緒對Condtion呼叫了signal()方法後
     * 該節點將會從等待佇列中轉移到同步佇列中,加入到對同步狀態的獲取中 
     */
    static final int CONDITION = -2;
    /**  
     * 表示下一次共享式同步狀態獲取將會無條件被傳播下去
     */
    static final int PROPAGATE = -3;
    /** 當前節點等待狀態 */
    volatile int waitStatus;
    /** 前驅節點 */
    volatile Node prev;
    /** 後繼節點 */
    volatile Node next;
    /** 節點關聯的執行緒 */
    volatile Thread thread;
    /** 
     * 等待佇列中的後繼節點,如果當前節點是共享的,那麼這個欄位是一個SHARED常量,即節點型別(獨佔和共享)
     * 和等待佇列中的後繼節點公用同一個欄位
     */
    Node nextWaiter;
    }
複製程式碼

node節點是構成同步佇列的基礎,pre、next前驅後繼維護了一個雙向佇列,同步佇列結構如圖:

Java併發——AbstractQueuedSynchronizer(AQS)同步器
當一個執行緒成功地獲取了同步狀態(或者鎖),其他執行緒將無法獲取到同步狀態,轉而被構造成為節點並加入到同步佇列中

獨佔式獲取與釋放同步狀態

  • 獨佔式獲取同步狀態
  • 獨佔鎖的lock方法都會呼叫AQS所提供的模板方法acquire(),當執行緒獲取同步狀態失敗後進入同步佇列中,後續對執行緒進行中斷操作時,執行緒不會從同步佇列中移出
    
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    複製程式碼
    模板主要思路:

    ①.呼叫tryAcquire()方法保證執行緒安全地獲取同步狀態(或者鎖),此方法自定義同步器自己實現
    ②.若獲取失敗,則構造同步節點,呼叫addWaiter()將該節點加入同步佇列尾部
    ③.最後呼叫acquireQueued()死迴圈獲取同步狀態
    ④.如果獲取不到,呼叫shouldParkAfterFailedAcquire()方法判斷是否需要阻塞,若返回true阻塞節點中的執行緒,可以依靠前驅節點的出隊或阻塞執行緒被中斷來喚醒阻塞執行緒

    tryAcquire

    tryAcquire()方法體內部只拋異常,若自定義同步器為獨佔式獲取同步狀態必須重寫此方法

    
        protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }
    複製程式碼

    addWaiter

    將節點加入同步佇列

    
        private Node addWaiter(Node mode) {
            //新建Node
            Node node = new Node(Thread.currentThread(), mode);
            // CAS快速嘗試尾插節點
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            //多次嘗試
            enq(node);
            return node;
        }
    複製程式碼

    若佇列為空或者cas設定失敗後,呼叫enq自旋再次設定

    
        private Node enq(final Node node) {
            // 死迴圈
            for (;;) {
                // 獲取尾結點
                Node t = tail;
                // 若佇列為空,初始化
                if (t == null) { // Must initialize
                    // cas設定頭節點
                    if (compareAndSetHead(new Node()))
                        // 設定尾結點
                        tail = head;
                } else {
                    // CAS設定尾結點
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    複製程式碼

    從原始碼中可以發現若同步佇列新增節點失敗後,會死迴圈一直尾插下去直至新增成功

    acquireQueued

    節點進入同步佇列之後,就進入了一個自旋的過程,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則會一直執行下去

    
        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);
            }
        }
    複製程式碼

    從原始碼中可以發現只有當前節點的前驅節點是頭節點才能嘗試獲取同步狀態,其原因在於:
    ①.頭節點是成功獲取到同步狀態的節點,而頭節點釋放同步狀態後,將會喚醒其後繼節點,後繼節點被喚醒後需要檢查自己是否為頭節點
    ②.保持FIFO同步佇列原則

    阻塞

    加入佇列後,會自旋不斷獲取同步狀態,但是自旋過程中需要判斷當前執行緒是否需要阻塞

    
        if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
        
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            // 前驅節點等待狀態
            int ws = pred.waitStatus;
            // 若前驅節點狀態為SIGNAL,表明當前節點處於等待狀態,返回true
            if (ws == Node.SIGNAL)
                return true;
            // 若前驅節點狀態>0即取消狀態,表明前驅節點已經等待超時或者被中斷了,需要從同步佇列中取消
            if (ws > 0) {
                // 迴圈遍歷,直至處於當前節點前面的節點不為取消狀態為止
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            // 前驅節點狀態為CONDITION,PROPAGATE
            } else {
                // CAS設定前驅節點狀態為SINNAL
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    複製程式碼

    若shouldParkAfterFailedAcquire返回true,會呼叫parkAndCheckInterrupt方法,其方法內部主要呼叫LockSupport工具類的park()方法阻塞執行緒

    
        private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            // 返回當前執行緒的中斷狀態
            return Thread.interrupted();
        }
    複製程式碼

    acquire()方法流程

    Java併發——AbstractQueuedSynchronizer(AQS)同步器

  • 獨佔式釋放同步狀態
  • 當執行緒獲取同步狀態後,執行完相應邏輯後就需要釋放同步狀態,AQS提供了release()方法釋放同步狀態,方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態),同樣自定義同步器需要重寫tryRelease()釋放同步狀態
    
        public final boolean release(int arg) {
            // 若釋放同步狀態成功
            if (tryRelease(arg)) {
                // 獲取同步佇列頭節點
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    // 喚醒頭節點的後繼節點
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    
    private void unparkSuccessor(Node node) {
        // 獲取當前節點等待狀態
        int ws = node.waitStatus;
        // 若狀態為SIGNAL、CONDITION或PROPAGATE,CAS將其狀態置為0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 獲取後繼節點
        Node s = node.next;
        // 若後繼節點為null或其狀態為CANCELLED(等待超市或者被中斷)
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 從尾結點遍歷
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 喚醒節點所關聯的執行緒
            LockSupport.unpark(s.thread);
    }
    複製程式碼
    複製程式碼

    複製程式碼

    從原始碼中可以發現喚醒的節點從尾遍歷而不是從頭遍歷,原因是當前節點的後繼可能為null、等待超時或被中斷,所以從尾部向前進行遍

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

    共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個執行緒同時獲取到同步狀 態,例如ReentrantReadWriteLock中的讀鎖

  • 共享式獲取同步狀態
  • 
        public final void acquireShared(int arg) {
            // 若獲取失敗自旋再次嘗試
            if (tryAcquireShared(arg) < 0)
                doAcquireShared(arg);
        }
    複製程式碼
    首先tryAcquireShared()嘗試獲取同步狀態,若返回值大於等於0時表明獲取成功,否則呼叫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) {
                        // 獲取同步
                        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);
            }
        }
    複製程式碼

  • 共享式釋放同步狀態
  • 
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                doReleaseShared();
                return true;
            }
            return false;
        }
    複製程式碼
    釋放了同步狀態之後,會喚醒後續處於等待狀態的節點,同樣自定義同步器需要重寫tryRelease()釋放同步狀態。不過因為是共享,會存在多個執行緒同時釋放同步狀態,所以採用CAS,當CAS操作失敗自旋循重試

    超時獲取同步狀態

    使用內建鎖synchronized同步,可能會造成死鎖,而AQS提供了超時獲取同步狀態,即在指定時間段內獲取同步狀態

  • 獨佔式超時獲取同步狀態
  • 相對於上面介紹的acquire()方法(此方法無法響應中斷),AQS為了響應中斷額外提供了acquireInterruptibly()方法,若當前執行緒被中斷會立即響應中斷丟擲異常
    
        public final void acquireInterruptibly(int arg)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }
    複製程式碼
    該方法首先判斷執行緒是否中斷,若是丟擲異常;否則執行tryAcquire()方法獲取同步狀態,獲取成功直接結束否則執行doAcquireInterruptibly(),與acquireQueued()類似,最大區別在於其不再使用interrupted標誌,直接丟擲InterruptedException異常
    
        private void doAcquireInterruptibly(int arg)
            throws InterruptedException {
            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;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        throw new InterruptedException();
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    複製程式碼

    tryAcquireNanos()方法超時獲取同步狀態是響應中斷獲取同步狀態的"增強版",增加了超時控制
    
        public final boolean tryAcquireNanos(int arg, long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            return tryAcquire(arg) ||
                doAcquireNanos(arg, 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);
        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);
        }
    }
    複製程式碼
    複製程式碼

    複製程式碼

    其思路:首先記錄deadline超時時間獲取同步狀態,若獲取失敗判斷是否超時,沒有超時則計算剩餘等待時間,若剩餘時間小於等於0表明已經超時,若沒有則判斷是否大於spinForTimeoutThreshold(1000L),如果大於使用阻塞方式等待,否則仍然自旋等待,使用了LockSupport.parkNanos()方法來實現限時地等待,並支援中斷

  • 共享式超時獲取同步狀態
  • 共享式獲取響應中斷doAcquireSharedInterruptibly()方法與共享式獲取同步狀態也類似,區別也是不再使用interrupted標誌,直接丟擲InterruptedException異常。共享式超時獲取大體思路也差不多,不再多述。

    感謝

    《java併發程式設計的藝術》

    相關文章