手撕AQS之Condition

Condition 的使用接觸最多是在 ReentrantLock,講 AQS 到這裏應該就完結了,前兩篇文章理了下 AQS 的共享鎖模式和獨立鎖模式,這篇在理下其中 Condition 的原理。


我在第一篇 手撕AQS之共享鎖 有提到同步操作的本質,大概遵循以下流程:

  1. 持有鎖;
  2. 操作共享資源;
  3. 釋放鎖;

無論是使用共享或者獨佔鎖,都無法在持有鎖的中途將鎖讓出,並繼續等待着獲取鎖。這就像使用 synchronized 同步對象一樣,但 synchronized 可以利用被同步對象的 wait 方法讓出鎖,當該對象的 notify 被調用時,它又能重新競爭鎖。所以,使用 ReentrantLock 怎樣實現這個呢?AQS 中實現了 Condition 接口的 ConditionObject 類能夠幫助我們做到。

整體思想ConditionObject 維護了條件隊列,當調用 await 時,將創建 node 並加入該條件隊列,當 signal 時,node 會從條件隊列取出,加入 CLH 隊列(這在前兩篇文章中有講),這和之前的邏輯並無兩樣。那現在,我們來看看關鍵的 await 和 signal 。

await:

		public final void await() throws InterruptedException {
            // 首先檢查線程是否中斷
            if (Thread.interrupted())
                throw new InterruptedException();
            // 創建 node 節點,添加到隊尾
            Node node = addConditionWaiter();
            // 1.釋放狀態值,並保存曾持有的狀態值
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 2.該節點不處於非同步隊列,暫停線程
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 3.請求入CLH 隊列
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  1. 釋放狀態值,並保存曾持有的狀態值,目的是爲了在重新獲得鎖後,恢復曾持有的狀態值:

    	final int fullyRelease(Node node) {
            boolean failed = true;
            try {
                int savedState = getState();
                // 真正的釋放鎖,會在方法內調用子類的實現
                if (release(savedState)) {
                    failed = false;
                    return savedState;
                } else {
                    // 這就是爲什麼調用 await 方法必須要獲取到鎖的原因
                    throw new IllegalMonitorStateException();
                }
            } finally {
                // 失敗的話,通過該狀態值,該節點能夠從隊列中移除
                if (failed)
                    node.waitStatus = Node.CANCELLED;
            }
        }
    
  2. 該節點不處於同步隊列(CLH 隊列),暫停線程:

    	final boolean isOnSyncQueue(Node node) {
            // 還是屬於條件隊列(prev可以是非空的,但不足以判斷其在隊列上,因爲把它放在隊列上的CAS可能會失敗。)
            if (node.waitStatus == Node.CONDITION || node.prev == null)
                return false;
            // 進入同步隊列,會爲該成員賦值,條件隊列是通過 `nextWaiter` 成員維護的
            if (node.next != null) // If has successor, it must be on queue
                return true;
            // 能到這裏,這個節點基本在隊列尾部
            return findNodeFromTail(node);
        }
    
    	// 從同步隊列循環查找該節點,如果存在,則返回 true
    	private boolean findNodeFromTail(Node node) {
            Node t = tail;
            for (;;) {
                if (t == node)
                    return true;
                if (t == null)
                    return false;
                t = t.prev;
            }
        }
    
  3. 請求入CLH 隊列,這就和 手撕AQS之獨佔鎖模式 中的一樣了。

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;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal 方法能夠轉變該節點的狀態,併入同步隊列:

	final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

文檔說的很清楚,我就保留了。需要解釋的是爲什麼前繼節點的 waitStatus 爲取消或者將前繼節點的 waitStatus 設置爲 SIGNAL 失敗,需要喚醒當前線程?成功的話,就將前繼節點更新爲 SIGNAL ,這是需要的,失敗的話,會重新執行下面這個 await 方法中的循環:

			while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
			private int checkInterruptWhileWaiting(Node node) {
            	return Thread.interrupted() ?
                	(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                	0;
        	}
			final boolean transferAfterCancelledWait(Node node) {
                // 上面這種情況這裏會失敗掉。
        		if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            		enq(node);
            		return true;
        		}
        		/*
         		* If we lost out to a signal(), then we can't proceed
         		* until it finishes its enq().  Cancelling during an
         		* incomplete transfer is both rare and transient, so just
         		* spin.
         		*/
        		while (!isOnSyncQueue(node))
	            	Thread.yield();
    	    	return false;
    		}

在最後帶來的影響也就是它跳出循環,進入競爭鎖的階段,在這個階段,還是有可能被阻塞。

只要進入同步隊列,就有機會被喚醒,然後在 await 方法中能夠結束:

			// 3.請求入CLH 隊列
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

如果是在同步隊列中喚醒該線程(與上面介紹的錯誤流程到這裏是不同的),也會進入到這裏,嘗試再次請求(acquireQueued),成功的話,就可以結束了,之前的線程就能恢復執行了。


總結

這裏比較繞的一個點就是線程恢復執行的問題。舉個列子,現在有線程A 調用 await 方法阻塞了,那麼它 park 的位置在 await 方法裏,如果線程 A 調用 lock 方法阻塞了,那麼它最後的 park 位置在 acquireQueued 方法裏調用的 parkAndCheckInterrupt 方法。而 await 被喚醒,會從 await 中結束掉,進入 acquireQueued方法, 如果最後沒有獲取到鎖,仍然會進入 parkAndCheckInterrupt 方法裏 park。這是比較重要的一個點,需要理解清楚!


我與風來


認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出

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