Java J.U.C 中 AQS 子類 ReentrantLock 源碼分析(一)

寫文章不易,轉載請標明出處。

同時,如果你喜歡我的文章,請關注我,讓我們一起進步。

一、概述

對於 Java 中的 JUC 包大家應該都是非常熟悉的,JUC 的全稱是 Java.util.concurrent ,翻譯過來也就是 Java 併發編程工具類包,在這個包中有許多在我們併發編程過程中經常使用到的線程安全的容器類和同步鎖等一些組件,而在這個包中很多的線程安全都是基於基礎底層的 AQS 來實現的,而 AQS 也就是 AbstractQueueSynchrogazer ,翻譯過來就是抽象隊列同步器,通過這個類的命名方式,我們就可以猜到它是一個抽象類。

在其使用的過程中,AQS 也是作爲一個抽象的基類,通過模板方法設計模式來實現子類最基礎的線程安全,並將具體的一些方法如 tryAcquire 或者 tryRelease 等交由子類去實現,讓子類根據自身功能的需要去定製實現相關的細節方法,而如果我們在這裏直接調取抽象基類 AQS 的一些方法則會直接拋出異常。

因爲 AQS 的實現子類非常的多,並且一些具體的獲取鎖和釋放鎖的實現都延遲到了子類中,因此在選擇源碼的閱讀時,選擇了比較常用的 ReentrantLock 。這個鎖是基於 Java 的一款顯示鎖,我們可以動態的對代碼進行加鎖和解鎖的操作,在編碼的過程中具備更高的靈活性(與之相對的是使用 C++ 在 JVM 中實現的 Synchrogazed 隱式鎖,該鎖完全由底層的 C++ 代碼實現,內嵌於 JVM 中,加鎖和解鎖的過程完全由 JVM 來進行控制,我們無法通過編寫代碼來進行干預)。

對於 ReentrantLock 的使用我們一般就是直接通過 lock 方法來進行加鎖,當需要同步的代碼執行完成後再使用 unlock 來進行解鎖,所以我們接下來代碼的分析主要就從這裏入手(示例代碼沒什麼太大作用,就是提供一個進入 ReentrantLock 的入口,我們從這裏開始閱讀源碼)。

最後說明一些編寫這篇博文的意義,因爲自己在網上也查找過比較多關於 AQS 的資料,也閱讀了一些書籍上的文章 ,但是普遍都感覺對於源碼的分析過於零散,大多是將代碼拆的零七八碎,把每一個方法都單獨拎出來說明其入參的含義或者代碼的邏輯,給人一種捨本逐末的感覺,個人認爲源碼的閱讀在於理解源碼編寫時的一種編程的思想,而這種思想應當是連續的,而不應該被肢解,所以我會盡量通過自己的理解並結合相關的資料以順序執行的方式來對這部分源碼的執行主流程進行分析,希望可以幫助到大家。

 

二、示例代碼

public class AQSDemo {
    public static void main(String[] args) {
        // 1.創建鎖
        final ReentrantLock lock = new ReentrantLock(true);
        Thread t= new Thread(){
            @Override
            public void run() {
                // 2.顯示加鎖
                lock.lock();
                // 3.執行邏輯代碼
                System.out.println("test");
                // 4.顯示解鎖
                lock.unlock();
            }
        };
        // 5.啓動線程
        t.start();
    }
}

 

三、源碼分析

3.1 創建 ReentrantLock 對象

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

首先創建 ReentrantLock 對象的時候會調用它的構造方法,通過上面的幾行代碼我們可以看到,默認情況下我們會調用 ReentrantLock 的無參構造器,這時創建出來的 ReentrantLock 中的 sync 屬性(Sync 是ReentrantLock的一個內部類)是一個非公平同步器,而當我們選擇使用有參構造器時,我們可以通過傳參的方式創建一個公平同步器

對於這裏的公平和非公平其實也就是我們說的鎖的公平性,對於 JVM 實現的 synchronized 內置鎖它是一款非公平鎖,而 ReentrantLock 既可以是公平鎖也可以是非公平鎖。一般來說,鎖的公平性是指一個先來後到的次序,我們都知道當在多線程併發的環境下,對於鎖住的資源我們會讓那個搶到鎖的線程來對其執行操作,而其餘的沒有搶到鎖的線程一般都會被方法哦一個 FIFO 的隊列當中進行排隊,等待再次被喚醒後繼續搶鎖。

那麼我們可以假設當一個線程剛好執行完畢後釋放鎖,此時一個線程進入,如果當前的鎖是公平鎖,那麼它就一定會到上面說到的隊列中排隊,此時被喚醒的應當是隊首的那個等待線程,換句話說也就是線程之間是平等的,大家都要排隊後才能拿到鎖(當隊列中仍存在線程等待時),這麼看來線程獲取鎖的操作就是公平的。相反,如果這個線程進入後沒有排隊,而是直接跟隊首的線程來進行搶鎖(搶成功了就它來執行,失敗就繼續排隊),那麼我們就說這種現象是不公平的,也就是一種非公平的鎖。具體的代碼實現我們在接下來閱讀 AQS 中 CLH 的代碼時可以遇到,這裏我們接着往下看。

3.2 創建 Sync 對象

從上面 ReentrantLock 構造器的代碼可以看到,當我們初始化 ReentrantLock 的時候,同時實例化了一個 Sync 對象並將其賦給了自己的屬性,那麼 Sync 對象是用來幹什麼的呢,我們直接來看代碼。

3.2.1 Sync

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            // ...
        }

        protected final boolean tryRelease(int releases) {
            // ...
        }

        // ...
    }

 Sync 類是 NonfairSync 和 FairSync 的共同父類,它繼承自 AQS 類,並且定義了抽象的 lock 方法。在這裏我們省略了 Sync 類中的部分方法的具體實現,主要關注其中比較重要的兩個方法,一個是 nonfairTryAcquire ,它是下面我們將要提到的 Sync 的一個具體實現類 NonfairSync 中 tryAcquire 實際調用的方法,而 tryRelease 這個方法正是重寫了父類 AQS 中的方法。

3.2.2 NonfairSync(非公平鎖)

    /**
     * Sync object for non-fair locks
     */
    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() {
            // 如果 CAS 操作成功說明當前線程獲取到鎖
            if (compareAndSetState(0, 1))
                // 保存當前線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 獲取失敗就執行與公平鎖相同的操作
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            // 調用父類 Sync 中的方法
            return nonfairTryAcquire(acquires);
        }
    }


    /**
     * 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;
    }

 首先我們先來看 NonfairSync ,也就是非公平同步器。根據上面對公平鎖和非公平鎖含義的介紹,大家應該已經可以瞭解這兩種鎖的區別了,通過這段代碼我們可以直接看到非公平鎖的執行邏輯,首先就像我們前面說的,新的線程來了之後立即就會去嘗試獲取鎖(公平鎖會先排隊),如果獲取鎖成功了,那麼就直接將當前的線程綁定到這個鎖的屬性上(通過 setExclusiveOwnerThread 方法),如果失敗了那麼久乖乖的去執行跟公平鎖一樣的排隊邏輯,那麼接下來我們再來看看公平同步器的實現。

3.2.3 FairSync(公平鎖)

    /**
     * Sync object for fair locks
     */
    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) {
            // ...
        }
    }

爲了和上面的非公平鎖的邏輯來進行對比,我們先將 tryAcquire 方法的代碼省略掉(這裏要注意,非公平鎖的 tryAcquire 方法實際上是調用父類 Sync 中的 nonfairTryAcquire 方法,而公平鎖的 tryAcquire 方法是直接重寫了父類 AQS 中的 tryAcquire 方法),在這裏我們就能很清楚的看到公平鎖和非公平鎖代碼的差異,即公平鎖永遠都是直接執行 acquire 添加隊列的邏輯,不會上來就直接通過 CAS 來搶鎖。

通過上面的分析,我們已經大致捋清了在示例代碼中使用 ReentrantLock 進行顯示加鎖的內部代碼邏輯,並且也知道了在 ReentrantLock 的構造器中會實例化一個 Sync 對象,並且這個 Sync 類就是 AQS 的一個子類,這樣我們就已經把 ReentrantLock 和 AQS 連接起來了,接下來我們繼續順着代碼的邏輯思路向下思考。

3.3 lock 方法

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


    /**
     * Sync object for fair locks
     */
    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) {
        // ...
        }
    }

首先,在示例代碼中我們在使用 ReentrantLock 都會去調用它的 lock 方法進行顯示的加鎖,在同步代碼執行結束後,再使用 unlock 進行顯示解鎖,因此我們接下來就進入到 ReentrantLock 的 lock 方法中。但是當我們跟進後會發現,其實 ReentrantLock 中的 lock 方法實際就是調用了我們剛剛實例化的 Sync 對象中的 lock 方法,所以我們繼續跟進代碼。

然後我們就會看到 不管是在 NonfairSync 還是在 FairSync 中,它們都調用了 acquire 方法,並且入參爲 1,所以我們這裏直接跟進 acquire 方法。

3.4 acquire 方法

    /**
     * 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) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire 方法是整個加鎖流程中至關重要的一個方法,它雖然只有幾行代碼,但是直接決定了當前線程獲取鎖的結果,在這方法裏面主要是一個 if 判斷語句表達式,表達式中的三個方法每一個的直接決定了最後的結果,因此這裏我們先介紹一下這幾個方法的大概作用:

(1)tryAcquire :嘗試獲取鎖(返回值取反);

(2)addWaiter :創建一個新的 CLH 節點,將線程保存後插入隊尾進行排隊;

(3)acquireQueued :自旋等待直到獲取鎖(返回值是線程在等待過程中是否被中斷過);

(4)selfInterrupt :恢復線程的中斷標記(因爲在 acquireQueued 中可能會被清除中斷標記);

這裏面需要注意的是 tryAcquire 方法,這個方法的返回值會在表達式中進行取反操作,也就是說當 tryAcquire 返回 true 時,即線程成功獲取鎖,不需要進行下面的 addWaiter 操作(表達式短路)。反之,如果 tryAcquire 返回 false ,那麼就說明線程獲取鎖失敗,就要執行 addWaiter 方法被添加到隊列中排隊。

因爲這幾個方法都很重要,所以我們接下來就挨個方法走進去看看。

3.5 tryAcquire(Fair 版本)

        /**
         * The synchronization state.
         * Belongs to ReentrantLock.
         */
        private volatile int state;        

        /**
         * 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()) {
                
                // 新的重入次數 = 原始重入次數 + 新增重入次數(這裏 acquires = 1)
                int nextc = c + acquires;

                // 如果計算後的重入次數小於零則直接拋異常
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
        
                // 設置重新計算後的重入次數
                setState(nextc);

                return true;
            }

            return false;
        }

這部分的代碼邏輯比較簡單,首先是先保存當前線程,然後獲取當前鎖的狀態,也就是 state 的值(state == 0 說明鎖未被佔用,state == 1 說明鎖已被佔用,當 state > 1 的時候說明此時的鎖爲重入狀態)。接下來的代碼執行就需要分不同的情況來看了。

首先我們假設此時鎖的狀態爲未被佔用(c == 0),那麼就會進入到第一個判斷體中,然後開始進行第二個判斷,第二步判斷中有一個 hasQueuedPredecessors 方法,它的主要作用是判斷當前線程是否需要排隊,因爲這個方法的情況比較複雜,我們把它放到下面去單獨講,這裏假設它返回了 true,也就是當前線程需要排隊,那麼在取反之後就變成了 false,所以結束判斷直接返回 false,又因爲我們在上面說過,當 tryAcquire 返回 false 的時候取反後爲 true,因此就會執行 addWaiter 方法去將該線程添加到等待隊列中。

然後我們再看另一種情況,假設此時鎖的狀態仍然爲未被佔用(c == 0),那麼還是進入到第一個判斷體中開始第二步判斷,這次 hasQueuedPredecessors 方法返回 false,也就是當前線程不要排隊,這樣取反後爲 true,代碼繼續執行 compareAndSetState 方法去獲取鎖,如果獲取鎖成功,那麼就直接調用 setExclusiveOwnerThread 方法將鎖與當前線程綁定,然後直接返回 true,這時 tryAcquire 返回值取反後爲 false,根據表達式短路定理,直接結束判斷,所以此時線程成功獲取鎖,不需要被添加到隊列中。

然後,如果此時鎖的狀態爲已被佔用(c != 0),並且結束第一個判斷之後第二個判斷成立(持有鎖的線程剛好是當前線程),那麼就直接執行重入鎖的邏輯,具體的邏輯代碼比較簡單,就不贅述了,需要注意的是這時 tryAcquire 方法返回的仍然是 true,也就是說在這種重入的狀態下,線程也是成功獲取到了鎖,不需要被添加到等待隊列中了。

最後一種情況,假如此時鎖的狀態爲已被佔用(c != 0),且持有鎖的線程不是當前線程,那麼兩個判斷語句都無法進入,則直接返回 false,然後通過 addWaiter 方法去執行添加等待隊列的邏輯。

 

3.6 hasQueuedPredecessors

    /**
     * Queries whether any threads have been waiting to acquire longer
     * than the current thread.
     *
     * <p>An invocation of this method is equivalent to (but may be
     * more efficient than):
     *  <pre> {@code
     * getFirstQueuedThread() != Thread.currentThread() &&
     * hasQueuedThreads()}</pre>
     *
     * <p>Note that because cancellations due to interrupts and
     * timeouts may occur at any time, a {@code true} return does not
     * guarantee that some other thread will acquire before the current
     * thread.  Likewise, it is possible for another thread to win a
     * race to enqueue after this method has returned {@code false},
     * due to the queue being empty.
     *
     * <p>This method is designed to be used by a fair synchronizer to
     * avoid <a href="AbstractQueuedSynchronizer#barging">barging</a>.
     * Such a synchronizer's {@link #tryAcquire} method should return
     * {@code false}, and its {@link #tryAcquireShared} method should
     * return a negative value, if this method returns {@code true}
     * (unless this is a reentrant acquire).  For example, the {@code
     * tryAcquire} method for a fair, reentrant, exclusive mode
     * synchronizer might look like this:
     *
     *  <pre> {@code
     * protected boolean tryAcquire(int arg) {
     *   if (isHeldExclusively()) {
     *     // A reentrant acquire; increment hold count
     *     return true;
     *   } else if (hasQueuedPredecessors()) {
     *     return false;
     *   } else {
     *     // try to acquire normally
     *   }
     * }}</pre>
     *
     * @return {@code true} if there is a queued thread preceding the
     *         current thread, and {@code false} if the current thread
     *         is at the head of the queue or the queue is empty
     * @since 1.7
     */
    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());
    }

對於 hasQueuedPredecessors 方法,與前面不同它是直接位於 AQS 基類中的方法,我這裏直接把源碼中整個方法加註釋全部拽了出來,目的不是爲了用註釋來嚇嚇你們,而是直接的證明一下這個方法的重要性。根據註解中的描述,我們可以大概的瞭解這個方法的作用,就是查詢是否有其它的線程已經比當前線程等待了更長的時間,而它的返回值即如果當前線程前面存在正在排隊的線程時返回 true,如果當前線程位於隊列的頭部或者隊列爲空時返回 false

根據上文之前的分析,那麼也就是說噹噹前線程前面存在排隊線程時,方法返回 true,外部取反後爲 false,因此 tryAcquire 方法整體返回 false,方法返回後在 acquire 方法的 if 判斷語句中會再次取反,所以此時爲 true,因此最後會調用 addWaiter 方法將當前線程進行排隊。反之如果當前線程位於隊首或者當前隊列爲空(或未被初始化),那麼返回 false,按照上面的邏輯返回後取反爲 true,因此會在 tryAcquire 方法的 if 判斷語句用繼續調用 CAS 來獲取鎖。

然後我們進入到方法體裏面來對方法的細節進行分析,首先前幾行代碼很容易理解,就是保存等待隊列的首尾節點便於後面使用,然後就是一個返回的表達式,這個表達式一共有三個小的判斷,但是每一個裏面都有很深的坑,所以我們一種情況一種情況的來進行分析,首先我們先分析第一個表達式 h != t 

(1)當隊列未被初始化的時候,此時 h 和 t 的值應當都爲 null,所以 h == t,那麼此時表達式直接短路返回 false,然後按照上面分析直接去通過 CAS 去獲取鎖(此時沒有等待隊列)。我們也可以這樣理解,當前沒有等待隊列,那麼也就意味着當前還沒有線程在等待,那麼新線程當然可以直接去嘗試獲取鎖。

(2)當隊列被初始化且隊列中的線程不大於一個的時候,此時 h 和 t 指向不同的節點(節點可以理解爲對線程包裝的對象),所以 h != t,然後繼續向下判斷 s == null(s 爲首節點的下一個節點,這個表達式就是判斷當前隊列中的節點數),唉?這裏就有問題了,這是在幹嘛,爲什麼要判斷首節點的下一個節點,而不是直接判斷首節點?其實這裏就有一個比較深的坑,涉及到 AQS 在設計 CLH 等待隊列時的一種思想,也就是首節點永遠不排隊,這一點可以怎麼理解呢,引用網上的一個例子,即當你在購買東西結賬的時候,排在隊列第一個的人應該是正在被服務的(正在結賬),所以從一定意義上來說他其實不算在排隊。也就是說,剛剛隊列中的第一個人,並不是正在排隊的第一個人,正在排隊等待的第一個人應該是整個隊列中的第二個人(因爲此時第一個人正在結賬)。

所以這樣我們也就可以理解爲什麼它是取首節點的下一個節點了,因爲下一個節點纔是真正在排隊的節點。而首節點應當是正在被執行的線程或者說正持有鎖的線程(首節點一般是被虛擬出來線程爲空或者是線程正持有鎖的節點,具體原因下面分析)。然後因爲我們假設當前等待隊列中的線程大於一個,所以 s != null ,那麼 s == null 判斷結果應爲 false。但是因爲後面整體是一個或表達式 ,所以我們還需要進一步判斷最後一個子表達式 s.thread != Thread.currentThread() ,這個表達式判斷的就是當前隊列中排在第二位(第一位正持有鎖,第二位正在等待鎖)的線程是不是當前線程,所以這裏也存在兩種情況。

第一種情況,假如當前排隊等待第一位(隊列第二位)不是當前線程,那麼 s.thread != Thread.currentThread() 表達式返回 true,然後 ((s = h.next) == null || s.thread != Thread.currentThread()) 表達式整體爲 true (false || true = true),那麼 hasQueuedPredecessors 方法返回 true,按照之前的分析,返回到 acquire 方法後最終會調用 addWaiter 方法去將當前線程排隊。

第二種情況,假如當前排隊等待第一位(隊列第二位)是當前線程(可重入狀態),那麼 s.thread != Thread.currentThread() 表達式返回 false,然後 ((s = h.next) == null || s.thread != Thread.currentThread()) 表達式整體爲 false (false || false = false),那麼 hasQueuedPredecessors 方法返回 false,按照之前的分析,返回到 tryAcquire 方法取反後爲 true,然後就會通過 CAS 來嘗試獲取鎖,如果獲取鎖成功(隊首剛剛正在執行的線程剛好釋放鎖)那麼 tryAcquire 方法整體返回 true,此時不再需要調用 addWaiter 進行排隊,反之如果 CAS 獲取鎖失敗(隊首線程仍在執行),那麼 tryAcquire 方法直接返回 false,之後會在 acquire 方法中調用 addWaiter 方法進行排隊。

這裏需要注意的是在第二種情況中,其實是一種可重入的狀態,即當前排隊的第一個(隊列的第二個)線程剛好是當前線程,那麼當該線程通過在 tryAcquire 方法中執行 hasQueuedPredecessors 方法確認可重入返回後,立即就可以通過 CAS 方法來嘗試獲取鎖。

(3)最後我們再來分析當隊列被初始化且隊列中的線程僅有一個的時候,此時 h 和 t 指向相同的節點,因此存在 h == t ,所以判斷語句的第一個條件 h != t 判斷爲 false,hasQueuedPredecessors 方法直接返回 false,然後直接在 tryAcquire 方法中通過 CAS 來嘗試獲取鎖。

但是我們可以思考一下,到底在什麼情況下,隊列纔會被初始化了但僅有一個線程?首先,當第一個線程到來時,因爲隊列還未被初始化,所以根據上面的代碼我們分析它是不會進行排隊的,通過接下來的代碼閱讀我們會發現隊列的初始化是在第二個線程到來的時候完成的。當第二個線程到來時,發現隊列未被初始化,但獲取鎖失敗時,他會初始化創建一個 CLH 隊列,並創建一個虛擬的節點(節點對應的線程爲 Null,但其實這個節點就對應着正在執行的第一個線程)加入隊列,然後自己也到隊列中進行排隊,所以當第二個線程到來時隊列中的線程也不是爲一。

此時還有一種情況,就是假如我們之前存在四個線程,在之後的運行過程中也不再有新的線程來獲取鎖,那麼當前三個線程運行結束,第四個線程會把自己設置爲首節點,並且因爲它的後面不再有等待的線程節點,所以此時隊列中就僅有一個線程節點,此時再有一個新的線程到來時,隊列中僅有一個線程節點,所以它會直接嘗試使用 CAS 獲取鎖,獲取成功拿到鎖,獲取失敗就去排隊。

3.7 addWaiter

    /**
     * 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
        // 創建新的 pred 變量保存 tail 隊尾節點
        Node pred = tail;
        
        // 如果隊尾節點不爲空則說明隊列已經被創建初始化過
        if (pred != null) {
            
            // 將當前線程節點的前置節點設爲原隊尾節點
            node.prev = pred;
            
            // 使用 CAS 算法將當前線程節點設置爲隊尾節點
            if (compareAndSetTail(pred, node)) {

                // 將原隊尾節點的後置節點設置爲當前線程節點
                pred.next = node;

                // 返回封裝後的當前線程節點
                return node;
            }
        }

        // 創建隊列並添加當前線程節點
        enq(node);

        // 返回封裝後的當前線程節點
        return node;
    }

 在看完了判斷語句中的第一個方法後,我們進入到下一個方法的研究,即當通過 tryAcquire 方法獲取鎖失敗後,會調用 addWaiter 方法來將當前線程封裝爲節點後添加到隊列中進行排隊。這個方法的代碼邏輯相對來說比較簡單,所以大部分的執行邏輯我都直接註釋在了代碼中。

代碼的邏輯,說白了就是現將當前線程封裝到節點中,然後判斷當前等待隊列是否被初始化,如果已經初始化過(尾結點不爲空)則直接通過 CAS 算法來將節點接到隊列的尾部,然後返回當前節點。反之,如果發現等待隊列尚未被初始化,則先調用 enq 方法來創建一個隊列,然後再將當前的節點接到隊列的尾部。

    /**
     * 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 死循環
        for (;;) {

            // 使用變量保存尾結點
            Node t = tail;

            // 尾結點爲空時(第一次循環,隊列未被初始化)
            if (t == null) { // Must initialize
                
                // CAS 算法創建並設置隊首節點(首節點對應線程爲 null)
                if (compareAndSetHead(new Node()))

                    // 將初始化後的隊首節點賦給隊尾節點
                    tail = head;

            } else {
                
                // 第二次循環時 t 不爲空是首節點,將當前線程節點的前置節點設爲 t
                node.prev = t;
                
                // CAS 算法將當前線程節點設置爲尾結點
                if (compareAndSetTail(t, node)) {
                    
                    // 將 t 的後置節點設置爲當前線程節點
                    t.next = node;

                    // 返回隊列的首節點
                    return t;
                }
            }
        }
    }


    /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

對於 enq 方法的邏輯稍微有一點點複雜, 比較有趣的是它是利用了 for 的兩次循環,當第一次循環時發現隊列未被初始化(隊尾節點爲空),則走第一個判斷語句塊直接創建一個節點(對應當前正在執行的線程,但是該節點的線程值爲 null)作爲首尾節點,在第二次循環時,發現隊列已經被初始化(尾結點不爲空),則走第二個語句塊將當前線程對應的節點接到隊列的尾部(關於 CLH 隊列的結構和代碼這裏不過多講解,之後專門會寫博文來介紹)。

3.8 acquireQueued

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {

        // 記錄標誌
        boolean failed = true;
        try {

            // 線程是否被中斷標誌
            boolean interrupted = false;

            // for 死循環
            for (;;) {
                
                // 獲取當前節點的前置節點
                final Node p = node.predecessor();

                // 如果前置節點爲首節點並且獲取鎖成功
                if (p == head && tryAcquire(arg)) {
                    
                    // 將當前線程節點設置爲首節點
                    setHead(node);

                    // 將原首節點後置節點設爲 null 來分離原首節點
                    p.next = null; // help GC

                    // 獲取鎖成功
                    failed = false;

                    // 返回線程中斷標誌
                    return interrupted;
                }

                // 檢測當前線程是否應當被阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    
                    // 阻塞當前線程並檢測當前線程是否被中斷
                    parkAndCheckInterrupt())
                    interrupted = true;

            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

接下來我們來分析 acquireQueued 方法,這個方法的作用正如其註釋所講主要是用於等待獲取鎖,它的返回值是在等待的過程中線程是否被中斷過。

首先代碼中上來就是一個 for 的死循環,當線程第一次進入時,會先獲取當前線程的前置節點,並判斷其是否爲首節點,如果當前線程的前置節點爲首節點,那麼就證明當前線程節點是隊列中的第二個節點,也就是正在等待獲取鎖而排隊的第一個節點,這時他會嘗試使用 tryAcquire 方法再次去獲取一次鎖,如果獲取成功那麼就直接斷開其前置節點,並將其設置爲前置節點,然後返回中斷標誌。

在這裏大家可能存在一個疑問,我們剛剛進入之前不是已經使用 tryAcquire 嘗試獲取過一次了嘛,爲什麼這裏還要再次獲取呢,我覺得可以這麼理解,在高併發的情況下,如果線程任務的執行速度特別快,可能在我們剛剛執行到這一步時,恰好它已經上升到了排隊的第一位,並且我們知道阻塞和喚醒線程也是存在性能消耗的,因此線程能可在 park 之前再嘗試一次,看看是否能夠獲取到鎖,這時如果能獲取到鎖就避免了阻塞和喚醒當前線程的性能消耗,直接讓當前線程拿到鎖,但如果其拿不到鎖,那也不要緊,就讓其繼續去排隊即可。

    /** 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;

    
    /**
     * 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;
        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;
    }


    /**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

再往下進入到了一個判斷語句,這個語句中一共有兩個方法,我們先看第一個 shouldParkAfterFailedAcquire ,這個方法的主要作用就像它的命名所說在獲取鎖失敗後返回當前線程是否應當阻塞。爲了便於理解,我將這個方法當中涉及的信號量 SIGNAL 的含義也貼在了上面,根據註釋翻譯過來,意思就是當一個節點爲這個狀態的時候,它的後置節點線程爲待喚醒狀態,需要注意的是這個信號量的值默認是爲 0 的。

因此我們分析這個方法,根據前置節點信號量的指引,如果當前線程的狀態爲 0(初始狀態),那麼會進入到最後一個判斷語句塊中,執行 compareAndSetWaitStatus 方法,來將前置節點的狀態設置爲 SIGNAL ,然後返回 false,也就是說當前節點不會被阻塞,而是會再進行一輪循環(說白了這輪循環的作用就是給前置節點打上信號量)。接下來再進行循環,再次獲取鎖,再次失敗後,再次進入到這個方法,此時前置節點的狀態應爲 SIGNAL ,那麼就意味着它目前的狀態是指明瞭它的後置節點是需要被喚醒的,所以這時當前節點就可以安全的阻塞了,所以此時返回 true

我覺得這裏之所以進行了兩次循環的判斷,主要的目的是爲了確保當前節點的前置節點的狀態爲 SIGNAL ,這樣才能保證在當前節點阻塞後,其前置節點能夠正常的將其喚醒。我們可以假設一下,假如當前節點的前置節點的狀態爲默認狀態,那麼噹噹前節點執行完畢後,可能不會去喚醒它的後置節點(因爲其狀態非 SIGNAL),這就會導致它的後置節點可能會一直被阻塞而無法被喚醒。


    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        // 阻塞當前線程
        LockSupport.park(this);

        // 返回線程的中斷標記
        return Thread.interrupted();
    }

當 shouldParkAfterFailedAcquire 返回 true 後就表明當前節點可以安全的(能夠被喚醒)阻塞了,這時就會調用 parkAndCheckInterrupt 方法來阻塞當前的線程,並返回該線程的中斷標記。這裏需要注意的是這點就是與正常的 CLH 隊列不一樣的地方,在正常的 CLH 隊列中,線程是不會被阻塞的,而是會一直的進行自旋,因此我們將 AQS 中的隊列成爲 CLH 的變種隊列

3.9 selfInterrupt

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

最後這個方法就沒有太多好說的了,就是當在發現當前線程被打了中斷標記後,中斷當前線程。到這裏爲止,ReentrantLock 的主要流程源碼分析就大致結束了。

 

四、內容總結

整個這篇博文大概寫了兩三天的時間,一邊閱讀代碼,一邊閱讀資料,一邊思考,一邊記錄,中間還經歷過一次因爲官方 Bug 導致中間一大部分內容丟失後又再次重寫,花費了比較長的時間。本來開始的意願是將這篇博文儘量的濃縮,文字簡潔一些,但是寫着寫着發現很多地方的很多情況不是三言兩語能解釋清楚地,因此又花了很多的文字去解釋他們。

因此這篇博文算是比較詳細的一篇初稿吧,內容總結我會放在之後的博文當中,打算再這篇博文的基礎上將 AQS 的內容再次進行一次濃縮,讓文章更加簡練一些。同時,後續我還會繼續對 AQS 中的 CLH 變種隊列和 AQS 中其他的一些分支代碼進行大概兩三篇博文的分析。

紙上得來終覺淺,絕知此事要躬行。

 

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