Java之AQS(一)


前言

AQS,抽象隊列同步器,通過維護一個鎖狀態state和一個雙向隊列,爲各種花裏胡哨的鎖(ReentrantLock,重入鎖;CountDownLatch,計數器;等等)提供了一些基本的實現(如:獲取鎖狀態;進入等待隊列;CAS操作的封裝;線程間的通信機制等等)。如圖所示,sync和Worker都是繼承於AQS。

在這裏插入圖片描述

在這裏插入圖片描述

如何使用

以一個互斥鎖爲例子,我們的Syn繼承了抽象隊列同步器並實現了其中的isHeldExclusively()方法,tryAcquire()方法及tryRelease方法這三個方法。這三個方法中,抽象隊列同步器都沒有爲我們提供具體實現,可以結合抽象隊列同步器提供的cas、狀態獲取等方法靈活實現。

/***
 *
 * @Author:fsn
 * @Date: 2020/4/26 18:44
 * @Description
 */


public class Mutex implements Lock {

    private final static int expect = 0;
    private final static int update = 1;

    private Syn syn = new Syn();

    @Override
    public void lock() {
        syn.acquire(update);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        syn.acquireInterruptibly(update);
    }

    @Override
    public boolean tryLock() {
        return syn.tryAcquire(update);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return syn.tryAcquireNanos(update, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        syn.release(update);
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    private static class Syn extends AbstractQueuedSynchronizer {
        // 1、AQS內部使用一個int成員變量代表同步狀態
        //2、AQS通過內置的FIFO隊列來完成線程的排隊工作

        @Override
        protected boolean isHeldExclusively() {
            // 是否保持獨佔
            return getState() == 1;
        }

        @Override
        protected boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 該狀態表示未有鎖競爭出現
            if (c==0) {
                // 進行CAS操作
                if (compareAndSetState(expect, update)) {
                    // cas成功, 設置爲獨佔
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
//          這裏也可以模仿重入鎖進行實現
//          else if (current == getExclusiveOwnerThread()) {
//                int nextc = c + acquires;
//                if (nextc < 0)
//                    throw new Error("Maximum lock count exceeded");
//                setState(nextc);
//                return true;
//            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            // 判斷當前線程是否爲獨佔的線程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 這裏模擬的是重入鎖的實現方式,其實這裏可以直接判斷
            // getState方法是否爲1,表示是否爲獨佔狀態
            int c = getState() - arg;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    }
}

其中,isHeldExclusively()方法,用於判斷線程是否爲獨佔,你可以根據狀態進行判斷,也可根據當前線程是否和獨佔線程進行判斷。這個狀態根據你加鎖時的設置而決定的,它作爲一個int類型的成員變量,默認值即爲0,一般情況下都習慣用0代表爲無鎖,1代表加鎖。

 protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

tryAcquire(),申請鎖方法也是沒有進行實現的,我們可以自己靈活實現,以文中互斥鎖的例子,從抽象隊列同步器裏頭獲取當前的狀態,爲0表示沒有加鎖,此時調用抽象隊列同步器的compareAndSetState方法進行CAS操作。cas操作採用Unsafe工具進行實現,其中compareAndSwapInt方法是一個用native修飾的本地方法。cas成功後,將當前線程設置爲獨佔的線程。

 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

分析

acquire

經過上面的例子,你可能對AQS有一個基本瞭解了,但這不還不是其強大的地方。如果有留意過重入鎖裏頭的lock方法的實現,你會發現它申請加鎖時還會調用抽象隊列同步器裏頭的acquire()方法,通過註釋,我們可以明白此方法是獨佔模式下線程獲取共享資源的入口,一開始還是會調用tryAcquire方法,如果獲取到了資源則直接返回,否則進入等待隊列,直至獲取到資源爲止。且這個過程忽略中斷的影響,如下代碼所示。

 /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        // 如果申請鎖不成功, 則放入阻塞隊列,這裏還是會調用tryAcquire()方法
        // 該方法也是我們自定義實現的
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

addWaiter

首先關於addWaiter()添加等待者方法, 該方法如下代碼所示,會創建一個 給定模式的等待者。結合addWaiter(Node.EXCLUSIVE),我們可以找知道該方法創建了一個 標記爲獨佔的節點,並進行入隊操作。

  /**
      * Creates and enqueues node for current thread and given mode.
      * 爲當前線程和給定模式創建並排隊節點。
      * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
      * @return the new node
      */
     private Node addWaiter(Node mode) {
        // 根據當前線程、模式創建節點
         Node node = new Node(Thread.currentThread(), mode);
         // Try the fast path of enq; backup to full enq on failure
         // 獲取隊列的尾部,不爲null, 則把當前節點的上一個節點設置爲尾部節點
         Node pred = tail;
         if (pred != null) {
             node.prev = pred;
             // 只有cas成功, 才能將尾部節點的下一個節點指向當前節點
             // 這裏cas操作企圖將當前節點設置爲尾部節點
             if (compareAndSetTail(pred, node)) {
                 pred.next = node;
                 return node;
             }
         }
         // 如果隊列爲空或者cas失敗則進入這個方法, 該方法源碼
         // 在模式定義源碼的後邊
         enq(node);
         return node;
     }

這裏的模式應該就是等待狀態的意思,它的幾種狀態如下所示,通過查看下方代碼,關於EXCLUSIVE的含義如下,注意這裏它這裏申明是null的。


static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        // 標明一個節點正在等待共享鎖
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        // 表明一個節點正在等待獨佔鎖
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        // 表明一個等待的線程被取消了
        static final int CANCELLED =  1;

        /** waitStatus value to indicate successor's thread needs unparking */
        // 標明一個等待線程的下一個線程需要被喚醒
        static final int SIGNAL    = -1;

        /** waitStatus value to indicate thread is waiting on condition */
        // 當前線程正在等待中
        static final int CONDITION = -2;

        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        // 下一次的acquire方法應該被無條件的傳播
        static final int PROPAGATE = -3;

        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
    
        
        // 以下省略
   }

enq

enq()源碼如下,這裏搞了一個死循環(自旋),如果第一次cas失敗會一直在這裏輪詢, 直到成功。

  /**
       * Inserts node into queue, initializing if necessary. See picture above.
       * @param node the node to insert
       * @return node's predecessor
       */
      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;
                  // cas成功, 再指向新節點. 雙向鏈表
                  if (compareAndSetTail(t, node)) {
                      t.next = node;
                      return t;
                  }
              }
          }
      }

acquireQueued

再回到這句代碼acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),我們可以知道它的含義就是,創建一個標記爲獨佔的節點,然後入隊。入隊之後我們得進行處理吧?不能讓每個線程就這樣乾等着吧。所以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;
                }
                // 判斷獲取鎖失敗之後是否可以進入等待喚醒狀態
                // 該方法保證當前線程的前驅節點的waitStatus屬性值爲SIGNAL,
                // 從而保證了自己掛起後,前驅節點會負責在合適的時候喚醒自己。
                if (shouldParkAfterFailedAcquire(p, node) &&
                        // 用於掛起當前線程,並檢查中斷狀態
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

源碼中,if (p == head && tryAcquire(arg)),爲什麼前驅節點爲頭結點就是嘗試獲取鎖呢?根據enq中的源碼,它其實就是一個無效節點,既然無效,當前節點理所當然可以獲取鎖。

 if (compareAndSetHead(new Node()))
              tail = head;

獲取鎖成功後,將當前節點設置爲頭節點,看看設置源碼,其實就是類似於出隊的操作,因爲當 前節點獲取鎖成功了,就應該繼續執行臨界資源了,就把它踢出去了。

   /**
      * Sets head of queue to be node, thus dequeuing. Called only by
      * acquire methods.  Also nulls out unused fields for sake of GC
      * and to suppress unnecessary signals and traversals.
      *
      * @param node the node
      */
     private void setHead(Node node) {
         head = node;
         node.thread = null;
         node.prev = null;
     }

shouldParkAfterFailedAcquire

再來看看shouldParkAfterFailedAcquire(p, node)方法和parkAnd CheckInterrupt()方法,其中,shouldParkAfterFailedAcquire涉及到了幾種狀態的轉換,再開始之前回顧一下它幾種狀態的含義:

// 表明一個等待的線程被取消了
        static final int CANCELLED =  1;

        /** waitStatus value to indicate successor's thread needs unparking */
        // 標明一個等待線程的下一個線程需要被喚醒
        static final int SIGNAL    = -1;

        /** waitStatus value to indicate thread is waiting on condition */
        // 當前線程正在等待中
        static final int CONDITION = -2;

        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        // 下一次的acquire方法應該被無條件的傳播
        static final int PROPAGATE = -3;

shouldParkAfterFailedAcquire方法有兩個作用,(1)判斷前驅節點等待狀態是否被取消了,如果是,需要移動節點;(2)嘗試將SIGNAL之外的有效狀態置成SIGNAL狀態

 /**
      * Checks and updates status for a node that failed to acquire.
      * Returns true if thread should block. This is the main signal
      * control in all acquire loops.  Requires that pred == node.prev.
      *
      * @param pred node's predecessor holding status
      * @param node the node
      * @return {@code true} if thread should block
      */
     private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
         // 獲取前驅節點的等待狀態
         int ws = pred.waitStatus;
         // 標明一個等待線程的下一個線程需要被喚醒, 確保前驅節點可以適當時機喚醒下一個節點
         if (ws == Node.SIGNAL)
             /*
              * This node has already set status asking a release
              * to signal it, so it can safely park.
              */
             return true;
          // 大於0的狀態只有取消cancel一種,所以將當前節點前移直到前驅是有效節點處
         if (ws > 0) {
             /*
              * Predecessor was cancelled. Skip over predecessors and
              * indicate retry.
              */
             do {
                 node.prev = pred = pred.prev;
             } while (pred.waitStatus > 0);
             pred.next = node;
         } else {
             /*
              * waitStatus must be 0 or PROPAGATE.  Indicate that we
              * need a signal, but don't park yet.  Caller will need to
              * retry to make sure it cannot acquire before parking.
              */
             // 嘗試將SIGNAL之外的有效狀態置成SIGNAL狀態
             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
         }
         // 沒有機會設置成SIGNAL, 繼續acquireQueued的自旋操作
         return false;
     }

parkAndCheckInterrupt

parkAndCheckInterrupt()方法用於掛起當前線程,並檢查中斷狀態,注意Thread.interrupted()並不是中斷線程的含義,只是判斷是否被中斷過。

 /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

cancelAcquire

接下來,如果是tryAcquire()拋異常了, 我們最終要取消申請鎖。然而,取消的方式也不是一個簡單過程,我們需要先找到前驅沒有處於取消狀態的節點,然後要把當前節點設置爲取消的狀態。接着,如果待取消的節點是尾部節點則比較好處理,只需要把找好的前驅節點的下一個節點設置爲null,如果是中間的節點,還得找一下繼任者,讓前驅節點連上繼任者。詳細信息可以看註釋。

 private void cancelAcquire(Node node) {
         // Ignore if node doesn't exist
         if (node == null)
             return;
 
         node.thread = null;
 
         // Skip cancelled predecessors
         Node pred = node.prev;
         // 如果前驅節點也是大於0表示取消狀態, 則一直向前找
         while (pred.waitStatus > 0)
             node.prev = pred = pred.prev;
 
         // predNext is the apparent node to unsplice. CASes below will
         // fail if not, in which case, we lost race vs another cancel
         // or signal, so no further action is necessary.
         Node predNext = pred.next;
 
         // Can use unconditional write instead of CAS here.
         // After this atomic step, other Nodes can skip past us.
         // Before, we are free of interference from other threads.
         node.waitStatus = Node.CANCELLED;
 
         // If we are the tail, remove ourselves.
         if (node == tail && compareAndSetTail(node, pred)) {
             compareAndSetNext(pred, predNext, null);
         } else {
             // If successor needs signal, try to set pred's next-link
             // so it will get one. Otherwise wake it up to propagate.
             int ws;
             // 如果前驅節點不爲頭結點
             if (pred != head &&
                     // 前驅節點的等待狀態處於SIGNAL狀態
                 ((ws = pred.waitStatus) == Node.SIGNAL ||
                         // 或者處於其他有效狀態, 嘗試將前驅結點設置爲SIGNAL
                  (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                     // 前驅節點的線程不爲null
                 pred.thread != null) {
                 // 獲取當前要取消申請鎖的節點的下一個節點
                 Node next = node.next;
                 if (next != null && next.waitStatus <= 0)
                     // 將事先找好的前驅節點的下一個節點設置爲當前取消申請鎖的節點的下一個節點
                     compareAndSetNext(pred, predNext, next);
             } else {
                 // 釋放之前喚醒繼任者
                 unparkSuccessor(node);
             }
             // 當前取消申請鎖的節點的下一個節點設置爲自身, 表示沒有被其他地方引用了
             node.next = node; // help GC
         }
     }

acquire總結

至此,我們重新回到原點,當申請鎖不成功, 則放入阻塞隊列,放入的過程中,我們已經知道會返回一箇中斷狀態表示是否被中斷,如果爲true,這裏selfInterrupt()才進行中斷操作。

 public final void acquire(int arg) {
        // 如果申請鎖不成功, 則放入阻塞隊列,這裏還是會調用tryAcquire()方法
        // 該方法也是我們自定義實現的
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  /**
      * Convenience method to interrupt current thread.
      */
     static void selfInterrupt() {
         Thread.currentThread().interrupt();
     }

總結

本文通過一個使用AQS的例子,分析了AQS所提供的一些基本操作,然後以acquire()方法爲切入點逐步分析了一個線程申請鎖時經歷的步驟。
其中:

- addWaiter負責將當前等待鎖的線程包裝成Node,並添加到隊列的末尾,enq方法通過自旋方式確保入隊成功,同時,enq方法同時還負責在隊列爲空時初始化隊列。
- acquireQueued方法用於在Node成功入隊後,繼續嘗試獲取鎖(當Node的前驅節點是head時才能獲取,這也符合了隊列設置的規則FIFO)或者將線程掛起。
- shouldParkAfterFailedAcquire方法用於保證當前線程的前驅節點的waitStatus屬性值爲SIGNAL(如果狀態不爲SIGNAL,則會嘗試將其他有效狀態改變爲SIGNAL),從而保證了自己掛起後,前驅節點會負責在合適的時候喚醒自己。
- parkAndCheckInterrupt方法用於掛起當前線程,並檢查中斷狀態。
- 如果獲取鎖的過程出現異常,則調用cancelAcquire方法取消獲取。

結束語

本文只分析了AQS中線程入隊及入隊後的操作,但這只是冰山一角,AQS還提供了Condition的操作,滿足我們在不同的條件下,讓不同的線程可以通過signal/await等方法相互協作。這裏計劃放到下篇文章進行分析。

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