AbstractQueuedSynchronizer 原理 & 源碼

閒話

看ThreadPoolExecutor 源碼的時候,其中 Worker 類是基於 AbstractQueuedSynchronizer 構建的,所以順便把這個類一起看了。另外,這個類也是ReenTrantLock 和 Semaphore 的底層的機制,說明足夠重要了。爲了方便,下文統一使用AQS 指代 AbstractQueuedSynchronizer 。

一、簡單瞭解

1.1、AQS —爲了同步而生

AQS 由 Doug Lea 大神編寫,主要目的是提供同步功能。AQS 內部維護了一個 int 類型的變量 state。這個 state 的不同值,可以代表不同的狀態,AQS 操作 state 變量時,使用 CAS,保證了原子性(多線程環境下安全)。通過在多線程環境下,對 state 不同的值代表的不同狀態進行賦予不同的含義,AQS 就可以當做一個比較完備的同步器來使用。這裏,對”狀態“這個詞做下解釋,舉兩個例子。第一個例子是互斥鎖:state 爲0,代表互斥鎖被佔用,state 爲1,代表互斥鎖可用。第二個例子是信號量(對共享資源的訪問):state 爲10,代表當前有10個資源(例如數據庫連接)可用,state 爲5,代表當前有 5個資源(數據庫連接)可用,state <= 0,則代表當前沒有資源可用,必須等待其他資源釋放後,才能使用。除了上述兩個場景外,其他同步場景,AQS 都可能作爲一個比較完美的方案來使用。
AQS 是一個抽象類,並提供了 互斥 和 共享 兩種同步場景。但是由於,AQS 的核心在於維護 state 變量,至於怎麼通過 state 來實現 互斥 或者 共享,AQS 並沒有過多幹涉,只是提供了一些空實現的方法,來讓子類具體實現。而且,AQS 提供了對條件變量的支持,可以說是很通用了。
我對與 AQS 的理解目前只到這一步,所以我認爲,AQS 比較重要的部分是,如何實現對狀態變量的操作和維護,以及中間的一些場景,例如,互斥 和 共享場景是如何實現的;如何支持的條件變量。這些點,也是下文要着重關注的一些點。

1.2、AQS 的繼承關係

AQS 繼承了 AbstractOwnableSynchronizer 類,AbstractOwnableSynchronizer 這個類提供了互斥的語義,比較簡單,這裏不做贅述。

二、AQS 的機制和源碼

2.1、AQS 類的結構

2.1.1、AQS 中 CLH 隊列節點結構

AQS 使用了 CLH 自旋鎖,去解決 互斥 和 共享 場景下的等待問題。

CLH CLH(Craig, Landin, and Hagersten locks): 是一個自旋鎖,能確保無飢餓性,提供先來先服務的公平性。
CLH鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。

節點的四個狀態:

  • a. CANCELLED = 1:因爲超時或者中斷,結點會被設置爲取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換爲其他狀態。處於這種狀態的結點會被踢出隊列,被GC回收;
  • b. SIGNAL = -1:表示這個結點的繼任結點被阻塞了,到時需要通知它;
  • c. CONDITION = -2:表示這個結點在條件隊列中,因爲等待某個條件而被阻塞;
  • d. PROPAGATE = -3:使用在共享模式頭結點有可能處於這種狀態,表示鎖的下一次獲取可以無條件傳播;
  • e. 0: None of the above,新結點會處於這種狀態。

Node 類的結構:

    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;

        /** 狀態位:線程(處在Condition休眠狀態)在等待Condition喚醒 */
        static final int CONDITION = -2;

        /**
         * 使用在共享模式頭結點有可能牌處於這種狀態,表示鎖的下一次獲取可以無條件傳播;
         */
        static final int PROPAGATE = -3;

        /**
         * CLH 節點的狀態位
         */
        volatile int waitStatus;

        /**
         * 前驅結點
         */
        volatile Node prev;

        /**
         * 後置節點
         */
        volatile Node next;

        /**
         * 當前線程
         */
        volatile Thread thread;

        // 下一個等待條件(Condition)的節點,由於Condition是獨佔模式,因此這裏有一個簡單的隊列來描述Condition上的線程節點。
        Node nextWaiter;

        /**
         * 返回是否是共享模式
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回前一個節點,如果爲空則拋出NullPointerException。當前任不能爲空時使用。可以省略null檢查,但它是用來幫助VM的。
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    
        }

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

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

2.1.2、AQS 的成員變量

// 等待隊列的頭結點
private transient volatile Node head;
// 等待隊列的尾結點
private transient volatile Node tail;
// 狀態
private volatile int state;

AQS 有3個比較重要的成員變量。

  • head CLH 隊列的頭結點
  • tail CLH 隊列的尾結點
  • AQS 的狀態變量

這3個變量都使用了 volatile 修飾,volatile 可以保證有序性、可見性,但不能保證原子性。爲了在多線程下,安全的使用這些變量,AQS 使用了 CAS 來操作這些變量。

2.2、互斥場景的實現

2.2.1、AQS 互斥場景下的相關方法

  • acquire(int arg) 申請獲取狀態(無法中斷)
  • acquireInterruptibly(int arg) 申請獲取狀態(支持中斷)
  • release(int arg) 釋放狀態

先附一張找到的流程圖,幫助理解代碼
在這裏插入圖片描述

下面我們着重看下 acquire(int arg) 和 release(int arg) 這兩個方法

2.2.2、acquire(int arg)

acquire(int arg) 源碼

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
        	// 生成節點,並加入隊列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

其中主要有tryAcquire(arg) 、acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 三個操作,下面挨個看下 ,首先是 tryAcquire(arg)

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

很明顯這個方法需要由子類自己去實現,實際上,AQS 狀態的變化也是在這個方法中完成的,明顯的模板方法模式。下面繼續看 addWaiter(Node.EXCLUSIVE) :

    private Node addWaiter(Node mode) {
        // 生成節點
        Node node = new Node(Thread.currentThread(), mode);
        // 獲取當前的尾結點
        Node pred = tail;
        // 如果尾結點不爲 null
        if (pred != null) {
            // 將新節點的前驅引用指向這個尾結點
            node.prev = pred;
            // CAS 將新節點設置爲尾結點
            if (compareAndSetTail(pred, node)) {
               // 將原來尾結點的 next 指針指向 新節點
                pred.next = node;
                return node;
            }
        }
        // 執行到這裏,說明要麼是 尾結點爲 null,要麼是 CAS 設置尾結點的時候失敗了
        enq(node);
        return node;
    }

下面看下 enq(node) 這個方法執行了:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                 // 如果尾結點 爲 null ,那麼首先要初始化 head 節點,然後將 tail 節點 指向 head,然後繼續循環
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// 如果尾結點不爲 null 了
            	// 將新節點的 前驅結點 指向 尾結點
                node.prev = t;
                // 設置新節點爲 尾結點
                if (compareAndSetTail(t, node)) {
                	// 將尾結點的 next 指針指向 新節點
                    t.next = node;
                    return t;
                }
            }
        }
    }

這樣,一個完整的 addWaiter(Node.EXCLUSIVE) 流程就走完了,主要涉及到節點的創建,以及head、tail節點的初始化以及新節點的入隊,並不是很麻煩。這個時候,我們回到 acquire(int arg) 方法,繼續看 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 做了什麼:

    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)) {
                 // 如果當前節點的前驅節點是 head 節點,並且成功申請了狀態,將進行以下操作:
                 // 將當前節點設置爲頭節點
                    setHead(node);
                    // 釋放 next 指針,幫助 GC 
                    p.next = null; 
                    // 修改標記位
                    failed = false;
                    // 返回中斷標記
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果 判斷當前節點需要阻塞 && 阻塞線程後,判斷該線程被中斷 ,則將中斷標記改爲 true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

比較重要的是 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 這兩個方法,下面一起看下:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	// 獲取 前置節點的狀態
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 如果狀態是 SIGNAL ,則說明需要前置節點 pred 來 喚醒 node,直接休眠
             */
            return true;
        if (ws > 0) {
            /*
             *ws >0 說明是 CANCELLED 狀態,則一直往前找,直到找到一個節點,而且這個節點的ws 非 CANCELLED 狀態的
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 捨棄中間的節點
            pred.next = node;
        } else {
            /*
             * waitStatus 一定是 0 或者 PROPAGATE。 這個時候,將前置節點的 ws 設置爲 SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

   private final boolean parkAndCheckInterrupt() {
   		// 阻塞當前線程
        LockSupport.park(this);
        return Thread.interrupted();
    }

shouldParkAfterFailedAcquire 方法,其實就是根據 node 的前置節點的 waitStatus 狀態位,來判斷是否需要休眠,當 waitStatus 爲 SIGNAL 時,線程將被休眠。這裏其實就是 AQS 對 CLH 鎖進行的變種,即後置節點並不會自旋,而是進行休眠,等可以申請狀態的時候,由前置節點進行喚醒。parkAndCheckInterrupt 方法就很簡單了,直接使用 LockSupport.park,將當前線程掛起。

到這裏,完整的互斥申請狀態位邏輯就結束了,下面,附一張找到的相關的流程圖,以幫助理解代碼。

2.2.3、release(int arg)

release 源碼:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

主要關注這幾個方法:

  • tryRelease 由子類實現,在這個方法裏修改 state
  • unparkSuccessor 喚醒後置節點

由於 tryRelease 需要子類實現,我們主要看下 unparkSuccessor 是如何喚醒後置節點的。
unparkSuccessor 源碼:

    private void unparkSuccessor(Node node) {
        // 獲取 節點的 waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
            // 如果 ws <0 ,則cas 操作 ws 更新成 0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 獲取後置節點
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            // 如果沒有後置節點,或者後置節點的 waitStatus >0 (爲CANCELLED),則 從隊列尾部向前遍歷找到最前面的一個 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);
    }

unparkSuccessor 代碼並不複雜,但是注意,當前節點的後置節點的 waitStatus 爲 CANCELLED 時,這時需要找到離當前節點最近的一個非 CANCELLED 狀態的節點,這個時候,需要從隊尾進行遍歷。具體原因可以看下 cancelAcquire 這個方法,這個方法,最後有一行 node.next = node; 相當於將被取消的節點的next 指針指向自己,這個時候如果從 head 遍歷,則會出現死循環,而從 tail 開始遍歷,則可以正常遍歷。

2.3、共享場景的實現

2.3.1、AQS 共享場景下的相關方法

  • acquireShared(int arg) 申請獲取狀態(無法中斷)
  • acquireSharedInterruptibly(int arg) 申請獲取狀態(支持中斷)
  • releaseShared(int arg) 釋放狀態

我們主要關注 acquireShared(int arg) 、 releaseShared(int arg) 兩個方法

2.3.2、acquireShared(int arg)

先附一張流程圖(來源:https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/index.html)
在這裏插入圖片描述

acquireShared 源碼

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

acquireShared 調用了 tryAcquireShared 和 doAcquireShared 兩個方法,同樣的,tryAcquireShared 需要由子類實現,在這個方法中改變state ,下面來看下 doAcquireShared 這個方法做了什麼。
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) {
                    // 如果前置節點是 head,則嘗試獲取狀態
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                       // 如果成功,則設置頭結點,並根據 Propagate ,來判斷是否需要喚醒其他線程
                        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);
        }
    }

與互斥場景下的狀態申請相比,acquireShared(int arg) 將 acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 統一寫在了doAcquireShared 方法中。但是,不同之處是,setHeadAndPropagate 中,會根據傳入的 propagate 進行等待節點的喚醒。下面看下 setHeadAndPropagate 的源碼。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
         // 這裏應該是判斷是否需要喚醒後置節點,但這裏的判斷我不是很理解,所以保留了原生的doc,下面是我理解的
        //1.propagate > 0 表示調用方指明瞭後繼節點需要被喚醒
        //2.頭節點後面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
       
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                // 喚醒後置的共享節點
                doReleaseShared();
        }
    }

其中的doReleaseShared(); 會喚醒線程,這個方法在下面的 releaseShared(int arg) 一起看。

2.3.3、 releaseShared(int arg)

releaseShared(int arg) 源碼

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

releaseShared(int arg) 中調用了 tryReleaseShared 、 doReleaseShared,其中tryReleaseShared 需要子類重寫,所以我們只關注 doReleaseShared ,看到底是怎麼進行線程喚醒的。
doReleaseShared 源碼.

    private void doReleaseShared() {
        for (;;) {
            // 獲取當前頭結點
            Node h = head;
            if (h != null && h != tail) {
                // 如果頭結點不爲null + 不是尾結點 + 頭結點的waitStatus是SIGNAL+CAS 設置waitStatus成功,就喚醒第二個節點
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    //如果頭結點的waitStatus是0,則 CAS 設置 waitStatus 爲 PROPAGATE,不成功,則繼續循環
                    continue;                
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

注意下,退出循環的條件是 h == head,這樣就會出現如下的情況:
當前 :head->a->b->c->d,此時,a 處於阻塞狀態
然後,有狀態釋放,這個時候,a被喚醒,同時,a成功獲取了狀態,成爲了首節點:head(a)->b->c->d 。由於setHeadAndPropagate 時,會喚醒 a 的後置節點 b,b成功獲取了狀態,隨後通過 setHeadAndPropagate 喚醒了 c,但是,c沒有獲取到狀態,重新回到休眠狀態。這個時候 a 釋放了持有的狀態。來個簡易圖 (爲了表示方便,使用 setHAP 代表 setHeadAndPropagate,使用 doRS 代表 doReleaseShared)
a->setHAP->doRS(此時,head 仍爲 a)                                                                  ->releaseShared->doRS(此時,head 爲b)
b                                                           ->setHAP->doReleaseShared->(此時,head爲b)

很明顯,a節點所在的線程,在做 releaseShared 時,隊列的 head 並不是它,而是 b。a 所在的線程,喚醒的其實是 c。

2.4、對條件變量(Condition)的支持

AQS 提供了Condition 接口的實現類,ConditionObject,每個 ConditionObject 中都維護了條件隊列。在分析 ConditionObject 類之前,我們需要理解 AQS 中的同步隊列(syn queue)和 ConditionObject 中的條件隊列(condition queue)之間的關係。

2.4.1、同步隊列 VS 條件隊列

同步隊列 和 條件隊列的鎖狀態以及聯繫

同步隊列節點:入隊(無鎖) —> 隊列中(獲取鎖) —>出隊(擁有鎖)
條件隊列節點:入隊(擁有鎖) —> 隊列中 (釋放鎖) —> 出隊(同其他線程爭奪鎖)

可以明顯的看出,同步隊列,入隊的時候是線程是沒有鎖的,但是出隊的時候,是擁有鎖的;相反,條件隊列,入隊的時候,線程是擁有鎖的,在隊列中將鎖釋放,出隊的時候,線程已經不再擁有鎖了。

一次普通的請求鎖+條件等待過程中,節點和隊列的變化:

Created with Raphaël 2.2.0開始請求鎖節點進入同步隊列同步隊列中獲取鎖離開同步隊列節點進入條件隊列條件不成立?條件隊列中釋放鎖條件達成(signal),節點加入同步隊列結束yesno

上述的過程中,除了最後的條件達成(signal)時,條件隊列中的節點轉移到同步隊列之外,基本上同步隊列 和 條件隊列是沒有太多交集的。

2.4.2、ConditionObject 的 類結構

        // 等待隊列的開始節點
        private transient Node firstWaiter;

        // 等待隊列的尾結點
        private transient Node lastWaiter;

ConditionObject 只有這兩個成員變量,即條件隊列的首尾節點,很簡單

2.4.3、ConditionObject 的等待(await())

看下,await() 方法的實現,await() 支持中斷的處理。

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
            	// 檢測到中斷,直接拋出異常
                throw new InterruptedException();
            // 將節點加入到條件隊列中(源碼放在這個方法下面了)
            Node node = addConditionWaiter();
            // 釋放當前的狀態
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
            	// 如果這個節點不在當前在同步隊列中,則掛起線程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 節點加入到 同步隊列 中,需要調用 acquireQueued 方法,嘗試獲取鎖
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
            	// 這裏已經獲取到了鎖,說明,節點已經從 同步隊列中移除,現在需要把通過 unlinkCancelledWaiters 把節點從 條件隊列中移除
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                // 根據interruptMode 處理中斷
                reportInterruptAfterWait(interruptMode);
        }
        
private Node addConditionWaiter() {
			// 獲取尾結點
            Node t = lastWaiter;
            // 如果最後一個節點被取消了,則清除取消節點
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 清除取消節點(源碼放下面了)
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 創建節點
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
            	// 初始化頭結點
                firstWaiter = node;
            else
                // 將節點放在尾結點後面
                t.nextWaiter = node;
            // 更新尾結點指針
            lastWaiter = node;
            return node;
        }
        
		// 取消取消狀態的節點,這個方法需要在持有鎖的情況下,才調用,整個過程並不會出現併發情況,也就沒有了線程安全的問題
        private void unlinkCancelledWaiters() {
        	// 獲取首節點
            Node t = firstWaiter;
            // 保存離 firstWaiter 最近的一個(包含 firstWaiter ),狀態爲 CONDITION 的節點
            Node trail = null;
            while (t != null) {
                // 獲取下一個節點
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    // 如果節點的waitStatus 不是 CONDITION,就將這個節點清除掉
                    // 將節點的後置指針情況
                    t.nextWaiter = null;
                    if (trail == null)
                    	// 如果當前尚未找到 trail (說明頭結點的狀態不是 CONDITION )
                        firstWaiter = next;
                    else
                        // 如果已經找到了 trail,則將 trail 的 nextWaiter 指針指向當前節點的下一個節點(跳過了本節點,相當於把本節點踢出了隊列)
                        trail.nextWaiter = next;
                    if (next == null)
                       // 如果當前節點已經是最後一個節點了,則更新 lastWaiter 指針
                        lastWaiter = trail;
                }
                else
                    trail = t;
                // 繼續下個節點
                t = next;
            }
        }

需要注意的地方:

  • await 方法在獲取到互斥鎖之後調用
  • 條件成立後,節點直接從條件隊列轉移到同步隊列

2.4.4、ConditionObject 的喚醒(signal())

signal() 源碼:

        public final void signal() {
            // 判斷線程是否持有鎖
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 獲取條件隊列的首節點
            Node first = firstWaiter;
            if (first != null)
            	// 喚醒首節點(源碼在下邊)
                doSignal(first);
        }
 
         private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                	// 如果需要喚醒的節點沒有後置節點,則直接更新 lastWaiter 指針爲null
                    lastWaiter = null;
                // 將first 的 nextWaiter 置爲null,幫助GC
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

    // 將節點從條件隊列 轉移到 同步隊列
    final boolean transferForSignal(Node node) {
        /*
         * cas 操作失敗(已經被轉移),返回false
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 節點加到同步隊列,並返回前驅結點
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        //  如果被取消或者 前驅結點的 CAS 操作失敗,則直接喚醒節點
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

2.4.5、圖解 await 和 signal 過程中發生的事

在這裏插入圖片描述

三、參考

  • https://segmentfault.com/a/1190000016462281#item-6-11
  • https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/
  • https://blog.csdn.net/yyzzhc999/article/details/96917878
發佈了52 篇原創文章 · 獲贊 7 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章