AQS很難,面試不會?看我一篇文章吊打面試官

小高先生o發表於2024-03-15

AQS很難,面試不會?看我一篇文章吊打面試官

大家好,我是小高先生。在這篇文章中,我將和大家深入探索Java併發包(JUC)中最為核心的概念之一 -- AbstractQueuedSynchronizer(AQS)。AQS不僅是構建JUC底層體系的基石,更是掌握併發程式設計不可或缺的一環,也是當下面試中常考問題。如果我們在學習JUC時忽略了AQS,那就像是基督教徒失去了耶路撒冷那般不可想象,它的重要性自不必多言。本文我將以ReentrantLock為切入點,深入討論AQS的原理和使用。本文內容多且複雜,為了方便大家學習,我在文章最後放置了ReentrantLock的流程圖,有助於大家更好的掌握AQS。

  • AQS概述
  • JUC基石-AQS
  • AQS重要變數
  • AQS原始碼深入分析
  • 總結

AQS概述

我們先從字面上解析AQS的含義。"Abstract"指的是抽象,這通常意味著AQS是一個旨在被繼承的抽象類,為子類提供共通的功能模板。緊接著,“Queued”詮釋了佇列的概念,暗示在高併發環境中,當多個執行緒競爭同一個資源時,未能獲取資源的執行緒將會被排列在一個阻塞佇列中,依次等待獲取機會。最後,“Synchronizer”即同步器,強調了AQS的設計初衷——為執行緒同步提供支援和框架。簡而言之,AQS是一個為同步而設計的抽象佇列同步器。

我們看一下AQS原始碼中的解釋:

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state. Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released. Given these, the other methods in this class carry out all queuing and blocking mechanics. Subclasses can maintain other state fields, but only the atomically updated int value manipulated using methods getState, setState and compareAndSetState is tracked with respect to synchronization

AQS提供了一個實現阻塞鎖和同步器的框架,它基於一個先進先出(FIFO)的佇列。在這個框架中,鎖的狀態是透過一個整型的原子變數state來表示的。執行緒可以透過請求獲取鎖和釋放鎖的方法來改變這個狀態。這個過程可以類比於小董在辦事大廳的一個服務視窗處理業務的場景。想象一下,在一個服務視窗,同一時間只允許一個人進行業務處理,而其他的人則需要在排隊區等待。為了簡單明瞭地指示當前視窗前的情況,我們可以用兩盞燈來模擬:綠燈亮時表示視窗無人使用,紅燈亮則意味著有人正在辦理業務。這與AQS中的狀態變數state的作用機制相似,用以指示鎖的佔用情況。

這個FIFO的雙向佇列是基於CLH單向連結串列實現的,我們透過包含顯式的("prev" 和 "next")連結以及一個"status"欄位,將其用於阻塞同步器,這些欄位允許節點在釋放鎖時向後續節點傳送訊號,並處理由於中斷和超時導致的取消操作。

JUC基石-AQS

為什麼說JUC的基石是AQS,JUC併發包中常用的鎖和同步器如ReentrantLockReentrantReadWriteLockCountDownLatchSemaphore等都是基於AQS實現的,以ReentrantLock為例,看下原始碼:

ReentrantLock有一個Sync變數,Sync繼承了AQS,所以我們呼叫ReentrantLock中的一些方法,就是在使用AQS的方法。其他的幾個類也都有Sync變數,也是繼承了AQS。你能順利簡單的使用這些工具類的方法,是AQS在為你負重前行。

AQS能幹的事兒一句話就能表明,多執行緒搶鎖就會有阻塞,有阻塞就需要排隊,實現排隊必然需要佇列。

在多執行緒環境之中,當多執行緒競爭同一資源時,通常需要一種機制管理這些執行緒的執行順序,以確保資源的有序訪問。這種機制需要一個佇列資料結構,用於儲存等待獲取資源的執行緒。這就是所謂的AQS同步佇列

AQS是一種用於構建鎖、訊號量等同步器的框架,它使用一個FIFO(先入先出)的佇列來管理等待的執行緒。當一個執行緒嘗試獲取被其他執行緒佔據的資源時,他會被放入這個佇列中,並進入等待狀態,就像去辦事大廳排隊等待的顧客一樣。一旦資源釋放,其他執行緒就有機會獲取資源。AQS的核心是狀態變數和節點類。每個節點代表一個等待的執行緒,包含執行緒的狀態資訊。狀態變數就表示資源的可用性,如資源被佔用或者未被佔用。AQS透過CAS、自旋、LockSupport.park()方法來維護狀態變數和節點佇列。

AQS重要變數

簡單看下AQS原始碼中重要的組成,一個靜態內部類Node,頭節點和尾節點說明佇列是雙向的,state為狀態變數,表示鎖是否被佔據。

附一張AQS的類結構圖

  • private volatile int state

AQS的同步狀態就是透過state實現的,就類似於辦事大廳中的視窗,用state表示是否有人在辦理業務。state = 0就是沒人,自由狀態可以辦理;state ≠ 0,有人佔用視窗,等著去。可以透過getState()setState()compareAndSetState()函式修改state。對於ReentrantLock來說,state就表示當前執行緒可重入鎖的次數。

  • AQS的CLH佇列

CLH佇列(三個狠人名字組成),用於儲存等待辦理業務的顧客。在CLH佇列中,每個節點代表一個等待鎖的執行緒,透過自旋鎖進行等待。state變數被用來表示是否阻塞,即鎖是否被佔用。我們來看一下原始碼裡CLH佇列的解釋:

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks. We instead use them for blocking synchronizers by including explicit ("prev" and "next") links plus a "status" field that allow nodes to signal successors when releasing locks, and handle cancellation due to interrupts and timeouts.The status field includes bits that track whether a thread needs a signal (using LockSupport.unpark). Despite these additions, we maintain most CLH locality properties.
To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you set the head field, so the next eligible waiter becomes first.

等待佇列是 "CLH"(Craig、Landin 和 Hagersten)鎖佇列的一種變體。CLH 鎖通常用於自旋鎖。 我們將其用於阻塞同步器,方法是加入顯式("prev "和 "next")連結和一個 "status "欄位,允許節點在釋放鎖時向後繼者發出訊號,並處理由於中斷和超時導致的取消。儘管增加了這些功能,但我們仍保留了大多數 CLH 本地化屬性。
要向 CLH 鎖傳遞佇列,可以原子方式將其拼接為新的尾部。要取消佇列,則需要設定頭部欄位,這樣下一個符合條件的等待者就會成為第一個。

CLH佇列的設計使得多個執行緒可以高效地競爭同一個鎖資源。由於每個執行緒只需要在自己的節點上進行自旋等待,而不需要遍歷整個佇列,因此減少了不必要的上下文切換和資源消耗。

  • Node節點類

Node類在AQS的內部,就是CLH佇列中的節點。Node節點就可以理解為辦理業務時等待去的椅子,每個椅子上坐一位顧客,裡面有顧客的等待狀態。

Node相關原始碼如下所示:

	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
         */
        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
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
        volatile int waitStatus;

        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. Assigned during enqueuing, and nulled
         * out (for sake of GC) only upon dequeuing.  Also, upon
         * cancellation of a predecessor, we short-circuit while
         * finding a non-cancelled one, which will always exist
         * because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.
         */
        volatile Node prev;

        /**
         * Link to the successor node that the current node/thread
         * unparks upon release. Assigned during enqueuing, adjusted
         * when bypassing cancelled predecessors, and nulled out (for
         * sake of GC) when dequeued.  The enq operation does not
         * assign next field of a predecessor until after attachment,
         * so seeing a null next field does not necessarily mean that
         * node is at end of queue. However, if a next field appears
         * to be null, we can scan prev's from the tail to
         * double-check.  The next field of cancelled nodes is set to
         * point to the node itself instead of null, to make life
         * easier for isOnSyncQueue.
         */
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

看看一些重要屬性:

  • EXCLUSIVE:表示一個獨佔節點,即只有一個執行緒可以獲取鎖資源,如ReentrantLock。當一個執行緒成功獲取鎖時,會建立一個EXCLUSIVE節點物件並將其設定為當前執行緒的節點狀態。當其他執行緒獲取鎖時,發現已經有執行緒持有了鎖,則將自身封裝成一個EXCLUSIVE節點並加入等待佇列中。
  • SHARED:表示一個共享節點,即多個執行緒可以同時獲取鎖資源,如ReentrantReadWriteLock。與EXCLUSIVE不同,SHARED允許多個執行緒同時持有鎖,但仍需要循序公平性。當一個執行緒請求共享鎖時,如果鎖是可用的,則執行緒可以直接獲取鎖;否則,執行緒會被封裝成一個SHARED節點並加入等待佇列中。
  • waitStatus:當前節點在等待佇列中的狀態。
    • 0:當一個Node被初始化時預設的狀態
    • CANCELLED:當節點在等待過程中被中斷或超時,它將被標記為取消狀態,此後該節點將不再參與競爭,其執行緒也不會再阻塞。
    • CONDITION:這表示節點當前在條件佇列中等待。執行緒執行了await()方法後,釋放了鎖並進入等待狀態,直到其他執行緒呼叫signal()方法。在條件佇列中的節點可以被移動到一個特殊的條件等待佇列,直到條件得到滿足。有關條件佇列的內容我將在之後的文章中講解。
    • SIGNAL:執行緒需要被喚醒
    • PROPAGATE:這個狀態通常用於共享模式,當一個執行緒釋放鎖或者資源時,如果頭節點是PROPAGATE狀態,它會將釋放操作傳播到後續的節點,以便這些節點也能嘗試獲取共享資源。

AQS原始碼深入分析

以最常用的ReentrantLock作為突破口進行解讀,分析AQS原始碼。

ReentrantLock實現了Lock介面,Lock透過聚合一個AQS的子類Sync實現執行緒訪問的控制。Sync又延申出公平鎖和非公平鎖。

構造方法

我們建立ReentrantLock,預設的構造方法會建立出非公平鎖。

public ReentrantLock() {
    sync = new NonfairSync();
}

如果建立公平鎖,建立的時候傳入true

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

lock()方法

如果呼叫lock(),可以看見底層呼叫的是Sync類中的lock()方法。

public void lock() {
    sync.lock();
}

Sync類中的lock()為抽象方法,有公平和非公平兩種實現方式,預設為非公平。在非公平鎖中,lock()方法中透過compareAndSetState(0, 1)來設定鎖的狀態,如果state在設定之前的值就是0,那就可以成功修改成1,如果設定之前不是0,則修改失敗,其實就是CAS演算法。第一個執行緒搶佔鎖,compareAndSetState(0,1)設定成功,當前執行緒搶到鎖。第二個執行緒呼叫compareAndSetState(0,1)就會設定失敗,進而呼叫acquire(1)

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

我們看下在公平鎖中,lock()就不一樣了,方法體內只有acquire()。我們可以先點進acquire(1)看下,進入之後再點選tryAcquire(),就會發現其實是AQS中的方法,只不過AQS這個父類並沒有有實現,而是在公平鎖類中重寫了tryAcquire()AQS類並沒有提供可用的tryAcquire()tryRelease()發法,正如AQS是鎖阻塞和同步器的基本框架一樣,tryAcquire()tryRelease()需要由具體子類實現

這種設計方式體現出一種設計模式,成為模板設計模式。在模板設計模式中,父類定義了一個演算法的骨架,而具體的實現細節則由子類完成。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                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;
    }
}

公平鎖和非公平鎖的主要區別在於它們在設定state之前的行為。公平鎖在嘗試獲取鎖之前,會先呼叫hasQueuedPredecessors()方法來檢查是否有其他執行緒在等待佇列中排隊等待獲取鎖。如果存在前驅節點(即有其他執行緒在等待佇列中),當前執行緒將不會嘗試搶佔鎖,而是加入到等待佇列的末尾,以確保按照請求鎖的順序來分配鎖資源,從而實現公平性。

相比之下,非公平鎖則沒有這個額外的檢查步驟。當一個執行緒嘗試獲取鎖時,它直接嘗試透過CAS操作來設定state,以搶佔鎖資源。這種方式可能導致多個執行緒同時競爭獲取鎖,而不考慮它們到達的順序,因此被稱為"群雄逐鹿"。

總結來說,公平鎖注重按照請求鎖的順序來分配鎖資源,保證先來後到的原則;而非公平鎖則允許多個執行緒自由競爭獲取鎖資源,不保證請求鎖的順序

看一下hasQueuedPredecessors(),如果當前執行緒之前有佇列執行緒,則返回 true;如果當前執行緒位於佇列頭部或佇列為空,則返回 false。

Returns:
true if there is a queued thread preceding the current thread, and false if the current thread is at the head of the queue or the queue is empty
如果當前執行緒之前有佇列執行緒,則返回 true;如果當前執行緒位於佇列頭部或佇列為空,則返回 false
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

無論是建立公平鎖還是非公平鎖,呼叫lock()方法進行加鎖,最終都會呼叫acquire()

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire()方法

acquire(1)中有兩個方法,分別是tryAcquire()acquireQueued()以及addWaiter(Node.EXCLUSIVE)。每一個方法都有自己的方法呼叫流程,我們看一下方法呼叫流程。

  • tryAcquire()

    這個方法之前說過,是由父類AQS提供,由NoFairSyncFairSync兩個子類實現方法。如果tryAcquire()搶鎖成功返回true,那acquire()方法也就做完了。如果搶鎖失敗,則執行acquireQueued()

    下面是非公平鎖呼叫tryAcquire()的流程,非公平鎖重寫了tryAcquire(),在裡面呼叫了nofairTryAcquire(),然後裡面就是透過CAS設定執行緒是否能佔用鎖。第一個執行緒搶鎖的時候,狀態為state為0,呼叫CAS方法將狀態為設定為1,設定成功後呼叫setExclusiveOwnerThread(),該方法的作用是設定當前擁有獨佔訪問許可權的執行緒。因為ReentrantLock是可重入鎖,所以如果判斷出state不為0,就會再判斷當前執行緒是否是鎖的持有者,如果是就將state加1,增加可重入次數,如果當前執行緒不是鎖的持有者,就return false

    公平鎖的tryAcquire()裡和非公平鎖基本一致,就是多了hasQueuedPredecessors()方法。

    		protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }        
    		/**
             * Performs non-fair tryLock.  tryAcquire is implemented in
             * subclasses, but both need nonfair try for trylock method.
             */
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    
        /**
         * Sets the thread that currently owns exclusive access. 設定當前擁有獨佔訪問許可權的執行緒。
         * A {@code null} argument indicates that no thread owns access.
         * This method does not otherwise impose any synchronization or
         * {@code volatile} field accesses.
         * @param thread the owner thread
         */
        protected final void setExclusiveOwnerThread(Thread thread) {
            exclusiveOwnerThread = thread;
        }
    
  • addWaiter(Node.EXCLUSIVE)

    在多執行緒環境下,當一個執行緒(比如執行緒A)成功獲取了鎖,而另一個執行緒(比如執行緒B)嘗試獲取鎖但沒有成功時,執行緒B會呼叫addWaiter(Node.EXCLUSIVE)方法。這裡的Node.EXCLUSIVENode類中的一個靜態屬性,它的值為null,這表示執行緒B正在以獨佔模式等待獲取鎖。

    addWaiter()方法中,執行緒B會檢查前驅節點(pred)是否為null。因為執行緒B是第一個嘗試獲取鎖但失敗的執行緒,所以佇列此時應該是空的,因此pred確實為null。在這種情況下,執行緒B將呼叫enq(node)方法將自己封裝成的節點加入到等待佇列中。

    進入enq()方法後,執行緒B首先判斷佇列的尾節點t是否為null。由於這是執行緒B首次嘗試加入佇列,所以t確實為null。然後,執行緒B呼叫compareAndSetHead()方法初始化佇列,建立了一個新的節點作為頭節點,並將tail設定為head。這個新建立的頭節點被稱為虛擬頭節點,它的作用是佔位,其Thread欄位為nullwaitStatus為0。雙向連結串列中,第一個節點為虛節點(也叫哨兵節點),並不儲存任何資訊,只是佔位。真正第一個有資料的節點,是從第二個節點開始。

佇列初始化完成後,執行緒B再次進入迴圈。這次,尾節點t不是null,因此執行緒B將進入else程式碼塊。在這裡,執行緒B將傳入的引數節點(即執行緒B自己)的prev指向t,也就是新傳入的節點的前向指標要指向當前的尾節點。然後,透過CAS演算法,執行緒B嘗試將自己設定為新的尾節點。如果成功,最後將原尾節點tnext指標指向執行緒B。

這樣,執行緒B就成功地將自己以獨佔模式等待獲取鎖的狀態加入到等待佇列中,等待有機會獲取鎖。

當後續的執行緒(比如執行緒C)也嘗試獲取鎖但未能成功時,它們會按照與執行緒B相同的流程加入到等待佇列中。實際上,後續執行緒的處理流程是固定的。首先,它們會設定當前節點的prev指標,然後透過呼叫compareAndSetTail()方法來嘗試將自身設定為新的tail節點。如果這個操作成功,接下來就會將前一個節點(即原來的尾節點)的next指標指向新來的執行緒節點,從而將新節點連結到佇列中。這樣,後續執行緒就順利地以獨佔模式等待獲取鎖的狀態加入到等待佇列中,排隊等待機會獲取鎖。

   /**
       * 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
          Node pred = tail;
          if (pred != null) {
              node.prev = pred;
              if (compareAndSetTail(pred, node)) {
                  pred.next = node;
                  return node;
              }
          }
          enq(node);
          return node;
      }
  
  	/**
  		為當前執行緒和給定模式建立併入隊節點。
  		引數:mode - Node.EXCLUSIVE 表示獨佔模式,Node.SHARED 表示共享模式
  		返回值:新建立的節點
  	*/
  	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;
                  }
              }
          }
      }

看這流程有沒有蒙,反正我一開始學的時候就很蒙了,大家多理解理解。

  • acquireQueued()

    acquireQueued()方法中,傳入引數為addwaiter()返回的節點。以執行緒B為例。首先,執行緒B呼叫predecessor(),得到執行緒B的前置節點,即虛擬頭節點。然後進入if判斷,雖然p是頭節點,但後面tryAcquire()搶鎖失敗。接著執行shouldParkAfterFaileAcquire(p, node)方法,此時p就是頭節點,也就是執行緒B的前置節點,而node則是當前執行緒B。

    shouldParkAfterFaileAcquire(p, node)方法中,會判斷p節點的waitStatus。此時waitStatus的值為0,因為節點初始化後waitStatus值為0。進入else程式碼塊後,將p節點的waitStatus設定為Node.SIGNALshouldParkAfterFaileAcquire(p, node)返回false,之後繼續做一次for迴圈。

    再次進入for迴圈之後,node仍為執行緒B,呼叫predecessor()得到的前置節點仍為虛擬頭節點。再次進入shouldParkAfterFaileAcquire(p, node),這次p的waitStatus為-1,等於Node.SIGNAL,方法返回true。接下來進入parkAndCheckInterrupt()方法,呼叫park()方法,將執行緒B掛起,使其進入等待狀態。當方法返回時,判斷執行緒B是否被中斷,如果被中斷則返回ture。至此,執行緒B才算真正進入等候區。

之後,執行緒C也會進入acquireQueued()方法,它的前置節點為執行緒B。在呼叫shouldParkAfterFaileAcquire(p, node)方法後,將B節點的狀態設定為Node.SIGNAL。再次進入shouldParkAfterFaileAcquire(p, node)方法後,B節點的狀態已經是Node.SIGNAL,然後呼叫parkAndCheckInterrupt()方法,C節點會呼叫park()方法進入等待狀態。

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);
    }
}

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;
        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.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

前面我們討論了很多內容,從acquire()方法開始。首先,透過tryAcquire()嘗試獲取鎖,如果失敗,則透過addWaiter(Node.EXCLUSIVE)建立節點並將其加入佇列。然後,呼叫acquireQueued()方法將搶鎖失敗的執行緒掛起。透過這三個方法,實現了將搶鎖失敗的執行緒入隊的操作。接下來,我們將學習鎖釋放後,等待佇列中的執行緒如何被喚醒並重新嘗試獲取鎖。

unlock()

現線上程A已經辦理完業務,呼叫unlock()方法釋放鎖。unlock()方法呼叫的是Sync的類方法release()

public void unlock() {
    sync.release(1);
}

release()是AQS提供的方,內部呼叫tryRelease()方法,與tryAcquire()類似,tryRelease()方法也需要在AQS的實現類重寫。當呼叫release()時,實際上是呼叫了Sync類重寫後的tryRelease()方法。

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #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;
}

來看一下在Sync中重寫後的tryRelease()方法。現在A執行緒要準備走了,釋放鎖。呼叫getState()方法,此時鎖的state為1,傳入的releases值也為1,c的值就是0。然後判斷當前執行緒是否是持有鎖的執行緒,如果不是會丟擲異常,正常情況下是不會出現這個問題的。之後執行if語句判斷c是否為0,此時c就是0,進入程式碼塊中執行setExclusiveOwnerThread(null)方法,這個方法將鎖的持有者設定為null。再呼叫setState(c)將鎖的狀態設定為0,返回true。至此執行緒A離開,鎖被釋放,其他等待執行緒就可以搶鎖了。release()方法中判斷tryRelease()返回的是true,就會進入程式碼塊中。將head頭節點賦值給h,h不為null並且waitStatus為-1,呼叫unparkSuccessor(h)

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

進入unparkSuccessor(Node node),傳入引數為head虛擬頭節點,其waitStatus為-1,所以要呼叫compareAndSetWaitStatus(node, ws, 0),將head的waitStatus設定為0。然後獲取node的後置節點,也就是執行緒B節點。s不為null並且s的waitStatus也不大於0,就不會進入if程式碼塊裡。由於s不為null,所以會呼叫unpark()喚醒B執行緒。

Wakes up node's successor, if one exists.
Params:
node – the node
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    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);
}

如果鎖的可重入次數不為1,也就是state不為1,tryRelease()返回false,release()方法也返回false,釋放鎖失敗。

喚醒B執行緒之後,回到之前的acquireQueued()方法,B執行緒會在for迴圈中繼續執行,重新嘗試呼叫tryAcquire()搶鎖,這次在if判斷中執行tryAcquire()可以成功,再次把鎖的state設定為1。

正常情況執行緒A釋放鎖之後就該執行緒B搶到鎖了,但是有極端情況,突然來個執行緒D把鎖搶走了,出現這種情況就是因為ReentrantLock是非公平鎖,會出現插隊的情況。

執行緒B搶到鎖之後就會呼叫setHead()離開佇列裡,去視窗辦理業務了。在setHead(Node node)中,將執行緒B設定為頭節點,並且將原來B節點的thread屬性設定為null,再將B節點的prev屬性設定為null。透過setHead()方法的操作,就可以將原來的B節點移除佇列,設定新的虛擬頭節點。接著會將p節點也就是原來的虛擬頭節點的next設定為null,這樣佇列中就不存在原來的虛擬頭節點了,而原來的執行緒B去視窗辦理業務了,他所在的node節點變成了虛擬頭節點。上述就是完整的解鎖過程。

/**
 * 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.
 *將佇列的頭部設定為node,從而脫離佇列。只能由acquire方法呼叫。為了GC和抑制不必要的訊號和遍歷,還將未使用的欄位清空。
 * @param node the node
 */
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

搶鎖入隊和釋放鎖出隊的正常流程都已經走完了,大家好好沉澱沉澱,把這部分捋順了,也這是一個學習閱讀原始碼的好機會。我們還差一個方法沒有看,就是在acquireQueued()方法中failed為true,就會呼叫cancelAcquire(Node node)方法,來研究一下cancelAcquire(Node node)方法。

cancelAcquire(Node node)

如果現在有三個執行緒節點在排隊,執行緒A、執行緒B以及執行緒C。執行緒B不想排隊,那它退出之後A就得指向C了,這個過程是有一些麻煩的,所以這個取消流程也比較重要。

  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;
        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 &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                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
        }
    }


下面我們分情況討論

  • 隊尾5號節點退出

    5號節點肯定不為null,將thread屬性設定為null。5號節點的前置節點是4號節點,4號節點的waitStatus不大於0,不會進入迴圈。4號節點的next為5號節點,將predNext設定為5號節點。將5號節點的waitStatus設定為Node.CANCELLED。進行if判斷,node為tail節點然後用caompareAndSetTail(node,pred)將尾節點設定為4號節點。再透過compareAndSetNext(pred,predNext,null)將4號節點的next設定為null。至此,5號節點完成退出。

  • 4號節點出隊

    4號節點的前一個節點是3號節點,正常情況下是不會進入while迴圈中的,但是有不正常的情況。4號節點退出的時候,3號節點也要取消,3號節點的waitStatus設定為Node.CANCELLED,所以可以進入while迴圈中,會把4號節點的prev設定為2號節點,就是要找到前面waitStatus不大於0的節點,也就是沒有取消的節點。

    還是假設3號節點沒取消,pred為3號節點,predNext為4號節點。將4號節點的waitStatus設定為Node.CANCELLED。4號節點不是尾節點,所以進入else程式碼塊。可以透過if判斷條件,next賦值為5號節點。if判斷也可以進入,就透過compareAndSetNext(pred, predNext, next)方法將3號節點(pred)的下一個節點(predNext)設定為next,也就是5號節點。

    最後將4號節點的next設定為node,也就是指向了自己,方便垃圾回收。

總結

整個ReentrantLock的加鎖過程,可以分為三階段:

  1. 嘗試加鎖
  2. 加鎖失敗,執行緒入佇列
  3. 執行緒入佇列之後,進入阻塞狀態

我在這裡給大家放一張加鎖和釋放鎖的流程圖,絕對有助於理解整個流程。大家也可以在學完這部分內容之後畫一張流程圖,梳理一下脈絡。

大多數開發者可能永遠不會直接使用AQS,但是直到AQS原理對於架構設計非常有幫助,學習之後我都覺得我長腦子了。

相關文章