AQS深入理解系列(四)Condition接口的實現

前言

一個新設計的出現,總是爲了替換現有的略有不足的設計。而Condition接口的出現,是爲了代替監視器鎖的wait/notify機制,提供更強大的功能。

JUC框架 系列文章目錄

與wait/notify進行對比

我們將Object自帶的wait/notify方法與Condition接口提供的await/signal進行一個對比。

Object方法 Condition方法 區別
void wait() void await()
void wait(long timeout) long awaitNanos(long nanosTimeout) 時間單位:前者毫秒ms,後者納秒ns
返回值
void wait(long timeout, int nanos) boolean await(long time, TimeUnit unit) 時間單位:前者只能納秒,後者什麼單位都可以
返回值
void notify() void signal()
void notifyAll() void signalAll()
- void awaitUninterruptibly() Condition獨有
- boolean awaitUntil(Date deadline) Condition獨有

先簡單說明一下它們之間的相同之處,以便之後更好地理解Condition接口的實現:

  • 調用wait()的線程必須已經處於同步代碼塊中,換言之,調用wait()的線程已經獲得了監視器鎖;調用await()的線程則必須是已經獲得了lock鎖。
  • 執行wait()時,當前線程會釋放已獲得的監視器鎖,進入到該監視器的等待隊列中;執行await()時,當前線程會釋放已獲得的lock鎖,然後進入到該Condition的條件隊列中。
  • 退出wait()時,當前線程又重新獲得了監視器鎖;退出await()時,當前線程又重新獲得了lock鎖。
  • 調用監視器的notify,會喚醒等待在該監視器上的線程,這個線程此後才重新開始鎖競爭,競爭成功後,會從wait方法處恢復執行;調用Condition的signal,會喚醒等待在該Condition上的線程,這個線程此後才重新開始鎖競爭,競爭成功後,會從await方法處恢復執行。

同步隊列 和 條件隊列

對於每個Condition對象來說,都對應到一個條件隊列condition queue。而對於每個Lock對象來說,都對應到一個同步隊列sync queue

sync queue

獨佔鎖的獲取過程中,我們提到了,每個線程在lock()嘗試獲取鎖失敗後,都會被包裝成一個node放到sync queue中去。
在這裏插入圖片描述
sync queue是一個雙向鏈表,它使用prevnext作爲鏈接。在這個隊列中,我們幾乎不關心節點的nextWaiter成員,最多會在共享鎖模式下,用來標識節點是否爲共享鎖節點。隊頭是一個dummy node即Thread成員爲null,第一個等待線程永遠只能是head的後繼。

condition queue

每一個Condition對象都對應到一個條件隊列condition queue,而每個線程在執行await()後,都會被包裝成一個node放到condition queue中去。
在這裏插入圖片描述
condition queue是一個單向鏈表,它使用nextWaiter作爲鏈接。這個隊列中,不存在dummy node,每個節點都代表一個線程。這個隊列的節點的狀態,我們只關心狀態是否爲CONDITION,如果是CONDITION的,說明線程還等待在這個Condition對象上;如果不是CONDITION的,說明這個節點已經前往sync queue了。

二者的關係

假設現在存在一個Lock對象和通過這個Lock對象生成的若干個Condition對象,從隊列上來說,就存在了一個sync queue和若干個與這個sync queue關聯的condition queue。本來這兩種隊列上的節點沒有關係,但現在有了signal方法,就會使得condition queue上的節點會跑到sync queue上去。
在這裏插入圖片描述
上圖簡單體現了節點從從condition queue轉移到sync queue上去的過程。即使是調用signalAll時,節點也是一個一個轉移過去的,因爲每個節點都需要重新建立sync queue的鏈接。

我們這裏可以先簡單理解一下關於隊列的動作:

  • 如果一個節點剛入隊sync queue,說明這個節點的代表線程沒有獲得鎖(嘗試獲得鎖失敗了)。
  • 如果一個節點剛出隊sync queue(指該節點的代表線程不在同步隊列中的任何節點上,因爲它已經跑到了AQS的exclusiveOwnerThread成員上去了),說明這個節點的代表線程剛獲得了鎖(嘗試獲得鎖成功了)。
  • 如果一個節點剛入隊condition queue,說明這個節點的代表線程此時是有鎖了,但即將釋放。
  • 如果一個節點剛出隊condition queue,因爲前往的是sync queue,說明這個節點的代表線程此時是沒有獲得鎖的。

CondtionObject

對於ReentrantLock來說,我們使用newCondition方法來獲得Condition接口的實現,而ConditionObject就是一個實現了Condition接口的類。

//ReentrantLock.java
public class ReentrantLock implements Lock, java.io.Serializable {
    public Condition newCondition() {
        return sync.newCondition();
    }

	abstract static class Sync extends AbstractQueuedSynchronizer {
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

ConditionObject又是AQS的一個成員內部類,這意味着不管生成了多少個ConditionObject,它們都持有同一個AQS對象的引用,這和“一個Lock可以對應到多個Condition”相吻合。這也意味着:對於同一個AQS來說,只存在一個同步隊列sync queue,但可以存在多個條件隊列condition queue

成員內部類有一個好處,不管哪個ConditionObject對象都可以調到同一個外部類AQS對象的方法上去。比如acquireQueued方法,這樣,不管node在哪個condition queue上,最終它們離開後將要前往的地方總是同一個sync queue

public abstract class AbstractQueuedSynchronizer{
	private transient volatile Node head;
	private transient volatile Node tail;
	
	public class ConditionObject implements Condition {
        private transient Node firstWaiter;
        private transient Node lastWaiter;
	}
}
  • firstWaiterlastWaiter分別代表條件隊列的隊頭和隊尾。
  • 注意,firstWaiterlastWaiter都不再需要加volatile來保證可見性了。這是因爲源碼作者是考慮,使用者肯定是以獲得鎖的前提下來調用await() / signal()這些方法的,既然有了這個前提,那麼對firstWaiter的讀寫肯定是無競爭的,既然沒有競爭也就不需要 CAS+volatile 來實現一個樂觀鎖了。
        final Lock lock = new ReentrantLock();
        Condition Emptycondition = lock.newCondition();
        Emptycondition.await();  //這樣會拋出異常

現在考慮沒有這個前提。上面代碼在沒有獲得鎖的情況就去調用了await,會導致await拋出異常,但是在拋出異常之前肯定會調用到addConditionWaiter,而addConditionWaiter有對這兩個變量的讀寫,現在可能同時有兩個線程對非volatile變量進行讀寫,也就可能造成問題。所以,在用戶使用不規範的情況下,還是有可能造成變量讀寫競爭,且沒有鎖保護的情況。(函數實現後面會講,請接着看)

public ConditionObject() { }

但ConditionObject的構造器什麼也不做。

await()第一次調用park之前

        public final void await() throws InterruptedException {
            // 在調用await之前,當前線程就已經被中斷了,那麼拋出異常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 將當前線程包裝進Node,然後放入當前Condition的條件隊列
            Node node = addConditionWaiter();
            // 釋放鎖,不管當前線程重入鎖多少次,都要釋放乾淨
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果當前線程node不在同步隊列上,說明還沒有別的線程調用 當前Condition的signal。
            // 第一次進入該循環,肯定會符合循環條件,然後park阻塞在這裏
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this); // 將阻塞在這裏
                // 如果被喚醒,要麼是因爲別的線程調用了signal使得當前node進入同步隊列,
                // 進而當前node等到自己成爲head後繼後並被喚醒。
                // 要麼是因爲別的線程 中斷了當前線程。
                // 如果接下來發現自己被中斷過,需要檢查此時signal有沒有執行過,
                // 且不管怎樣,都會直接退出循環。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            /*之後再講後面的部分*/
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

註釋介紹了大概的過程,但首先要明確會有哪些線程在執行:

  • 執行await的當前線程。這個線程是最開始調用await的線程,也是執行await所有調用鏈的線程,它被包裝進局部變量node中。(後面會以node線程來稱呼它)
  • 執行signal的線程。這個線程會改變await當前線程的node的狀態,使得await當前線程的node前往同步隊列,並在一定條件在喚醒await當前線程。
  • 中斷await當前線程的線程。你就當這個線程只是用來喚醒await當前線程,並改變其中斷狀態。
  • 執行unlock的線程。如果await當前線程的node已經是同步隊列的head後繼,那麼獲得獨佔鎖的線程在釋放鎖時,就會喚醒 await當前線程。

理解了這幾個線程的存在,對於本文的理解有很大幫助。從用戶角度來說,執行await \ signal \ unlock的前提都是線程必須已經獲得了鎖。

addConditionWaiter

        private Node addConditionWaiter() {
            Node t = lastWaiter;//獲得隊尾
            // 同步隊列中的節點只是CONDITION的,就認爲是以後將離開條件隊列的節點。
            // 將調用unlinkCancelledWaiters來一次大清理,並重新獲得隊尾
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 包裝當前線程爲node,狀態初始爲CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)// 如果當前隊尾爲null,那麼整個條件隊列都沒初始化呢
                firstWaiter = node; //把新建node作爲隊頭
            else // 如果隊列有至少一個節點
                t.nextWaiter = node;  // 把新建node接在隊尾後面
            lastWaiter = node; //讓node成爲新隊尾
            return node;
        }

上面函數描述了新建node加入到條件隊列中的過程,我們和獨佔鎖獲取過程中新建node加入同步隊列進行對比:

  • 同步隊列sync queue的新建node,它的初始狀態爲0。而條件隊列condition queue的新建node的初始狀態爲CONDITION
  • sync queue如果擁有隊頭,隊頭肯定會是一個dummy node(即線程成員爲null)。condition queue則不會有一個dummy node,每個節點的線程成員都不爲null。
  • sync queue是一個雙向鏈表,需要維持前驅和後繼都正確。condition queue只是一個單鏈表,只需要維持後繼即可。

這裏先提前說下,中斷await當前線程的線程(這裏特指中斷操作在signal之前)和執行signal的線程都會使得條件隊列上的node的狀態從CONDITION變成0。

unlinkCancelledWaiters

unlinkCancelledWaiters函數是用來從頭到尾清理狀態不爲CONDITION的節點的。

        private void unlinkCancelledWaiters() {
            Node t = firstWaiter; //獲得隊頭
            Node trail = null; //trail用來保存遍歷過程中,最近一次發現的狀態爲CONDITION的節點
            while (t != null) { //只要循環變量不爲null,循環繼續
                Node next = t.nextWaiter; //得到循環變量的後繼,循環結束前使用
                // 如果循環變量不爲CONDITION
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;  //首先使得t與後繼斷開鏈接
                    // 如果直到當前循環都還沒有發現一個CONDITION的節點
                    if (trail == null)
                        firstWaiter = next;//那麼將循環變量的後繼,作爲新隊頭
                    // 如果當前循環之前已經發現了一個CONDITION的節點
                    else
                        trail.nextWaiter = next;//那麼將trail與next相連,相當於跳過了循環變量t
                    // 如果已經遍歷到隊尾,需要將trail作爲隊尾,因爲trail纔是隊列中最後一個爲CONDITION的節點
                    if (next == null)
                        lastWaiter = trail;
                }
                // 如果循環變量爲CONDITION,則更新trail
                else
                    trail = t;
                t = next;  //循環結束前,用next更新循環變量
            }
        }

主要是一些單鏈表的操作,trail變量很重要,它用來保存遍歷過程中,最近一次發現的狀態爲CONDITION的節點。

fullyRelease

在調用fullyRelease之前,當前線程已經被包裝成node放到條件隊列中去了。注意,在這個函數以後,我們再也不會對firstWaiterlastWaiter輕舉妄動了,因爲以後的執行過程中,當前線程很可能是沒有持有鎖的。

我們調用fullyRelease函數來釋放當前線程佔有的鎖。

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();//這裏會獲得重入鎖的總次數
            if (release(savedState)) {
                failed = false;
                return savedState;// 返回重入鎖的總次數
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

該函數很簡單,只是通過調用獨佔鎖釋放過程中的release函數來釋放鎖,注意,不管鎖被重入了幾次,在這裏我們都會一次性釋放乾淨的(release(savedState))。這也是爲什麼這個函數叫做fullyRelease全釋放。

如果真的有哪個傻子在沒有獲得鎖時,調用了await,那麼release會拋出異常且導致拋出時failed變量爲真,那麼在finally塊裏面就會執行語句,把當前線程的node的狀態變成非CONDITION的。

isOnSyncQueue

在執行完fullyRelease後的這一段時間裏,當前線程是沒有持有鎖的了,因爲鎖已經被自己給釋放了。更重要的是,接下來的這一段時間裏,另一個線程可能又獲得了鎖,然後開始執行await \ signal \ unlock,即接下來得考慮多線程了。

回到await的邏輯,現在要進入循環了。循環裏馬上就會調用LockSupport.park(this);阻塞當前線程,這也就是本章大標題說的“await()第一次調用park之前”的時間點了。

但是每次循環都會判斷一下循環條件!isOnSyncQueue(node),即當前線程node不在同步隊列中。很明顯,如果是第一次進入循環,這個循環條件肯定會滿足的,因爲我們剛剛纔執行了addConditionWaiter將當前線程node加入到條件隊列中呢。

這個循環條件!isOnSyncQueue(node)主要是爲了當前線程被喚醒後,進行必要的判斷。

    final boolean isOnSyncQueue(Node node) {
        //如果node狀態爲CONDITION,或者雖然node狀態不爲CONDITION,但node前驅爲空
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //執行到這裏,說明上面兩個條件都不成立:
        //1. node狀態不爲CONDITION 2. node前驅不爲空

		//如果node後繼不爲空,說明已經在sync queue
        if (node.next != null) 
            return true;
        //執行到這裏,說明:
        //前一個時間點檢測到,1. node狀態不爲CONDITION 2. node前驅不爲空
        //後一個時間點檢測到:node後繼爲空

		//現在發現node處於一個狀態:前驅不爲空,但後繼爲空。如果node是當前隊尾肯定也是這種狀態
		//但enq進隊尾時CAS設置tail失敗時,也會是這種狀態。所以需要從尾到頭檢測一遍。
        return findNodeFromTail(node);
    }

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

注意,本函數都是在檢測prev next兩個鏈接,即sync queue的數據結構。

  • if (node.waitStatus == Node.CONDITION || node.prev == null)分支:
    • 先看看進入分支的情況:
      • 如果前者成立,即node狀態爲CONDITION。說明node代表線程還沒有前往同步隊列。
      • 如果前者不成立,後者成立,即雖然node狀態不爲CONDITION,但node前驅爲空。一個節點如果已經入隊成功,那麼它的prev肯定不爲null。
    • 再看看退出這個分支的情況:
      • 中間條件是或,所以是二者都不成立。即1. node狀態不爲CONDITION 2. node前驅不爲空。但如果只是已知這些信息,則還需要繼續判斷。
  • if (node.next != null)分支,如果說node的next都已經不爲空了,說明node成爲隊尾後,又有節點入隊成爲新隊尾。而發生這一切的前提則是,node已經成功入隊過了。
  • 最後需要從尾到頭遍歷,看是否能在同步隊列上找到node。執行findNodeFromTail之前,發現node處於一個狀態:前驅不爲空,但後繼爲空。如果node是當前隊尾肯定也是這種狀態。但enq進隊尾時CAS設置tail失敗時,也會是這種狀態。所以需要從尾到頭檢測一遍。

關於狀態不爲CONDITION,前面有說過,有兩種線程可以使得條件隊列上的node的狀態從CONDITION變成0。但現在可以排除中斷await當前線程的線程這種情況(之後具體介紹這個流程),因爲如果發生了中斷,await的while循環直接就會break出循環了(if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;),也就不會執行到isOnSyncQueue函數了。

簡單的說,isOnSyncQueue判斷節點已經入隊同步隊列的標準,必須是node已經成爲隊尾(包括當前是隊尾,或者曾經是隊尾)。

await()第一次調用park之後

正常情況下,await()第一次調用park之後,就會阻塞在這裏了,所以這裏必須依靠別的線程出來救場了。

此時node的狀態爲:

  • 在數據結構上,node在條件隊列上(addConditionWaiter)。
  • 在執行過程上,node線程當前阻塞在LockSupport.park(this)這裏。

signalAll流程

前面我們提前說過執行signal的線程,我們先來看看執行signalAll的線程會幹什麼。

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

首先會檢查執行signalAll的線程是否已經獲得了鎖,通過判斷ExclusiveOwnerThread成員變量。然後判斷條件隊列是否爲空,只要不爲空就執行doSignalAll

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

上來就把代表條件隊列的隊頭隊尾成員置null,之後別人就無法通過隊頭隊尾找到隊列中的節點了,只有當前線程能通過局部變量first來找到隊列節點了。

而接下來不斷遍歷,直到已經遍歷到隊尾(first != null)。每次遍歷中,將當前遍歷節點 與 剩下的條件隊列鏈 斷開,然後對當前遍歷節點執行transferForSignal

    final boolean transferForSignal(Node node) {
        /*
         * 如果失敗,說明node代表線程因中斷而已經執行了中斷流程中的compareAndSetWaitStatus(node, Node.CONDITION, 0)
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 執行enq將node入隊sync queue,enq返回node的前驅。
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

transferForSignal函數簡單的說,就是爲讓參數node入隊,如果入隊成功就返回true。

  • 如果CAS設置node狀態從CONDITION變成0失敗了,說明node代表線程因中斷而已經執行了中斷流程中的compareAndSetWaitStatus(node, Node.CONDITION, 0)這也間接說明了signal流程和中斷流程都是以 成功設置node狀態 作爲標準,哪個流程成功了,哪個流程就把node入隊同步隊列,從而以免重複入隊。(這一點需要和後面的中斷流程內容聯動,如果難以理解,可以直接往下看)
  • 如果CAS設置成功,那麼enq(node)入隊,然後肯定返回true。但是注意,在一定條件下,會喚醒node代表線程。注意enq(node)返回node入隊後的前驅prev
    • 這個一定條件是指,node的前驅狀態是同步隊列節點的取消狀態,或者狀態<=0但CAS設置前驅狀態爲SIGNAL失敗了。
    • 如果上面條件發生了,就直接喚醒node線程。這裏我們回想一下await()第一次調用park之後這個時間點,現在node線程終於被喚醒,假設沒有中斷髮生過的話,不會因break退出循環,再一次檢測!isOnSyncQueue(node)會發生條件不成立,因爲node已經因爲enq(node)而成功入隊。然後又會走到獨佔鎖獲得過程中的acquireQueued函數。
    • 喚醒node代表線程不一定代表它接下來能夠獲得鎖,但是我們也不用擔心這會有什麼壞影響,因爲acquireQueued函數自己會去做判斷,如果發現還是獲取不到鎖的話,則會調用shouldParkAfterFailedAcquire將node的前驅設置爲SIGNAL的。
    • 總之,compareAndSetWaitStatus(p, ws, Node.SIGNAL)直接保證了node的前驅狀態爲SIGNAL,而LockSupport.unpark(node.thread)間接保證了node的前驅狀態爲SIGNAL,之所以說間接,是因爲這不是在signal線程裏做的,而是通過喚醒node線程做到的。

簡單總結一下signalAll方法:

  1. 將條件隊列清空(通過lastWaiter = firstWaiter = null來達到效果,但函數中的局部變量已經保存了隊頭,且實際上節點的鏈接還存在着)。
  2. 遍歷每個節點。
  3. 如果遍歷節點已經被取消掉了(compareAndSetWaitStatus(node, Node.CONDITION, 0)失敗),那麼直接返回,處理下一個節點。
  4. 如果遍歷節點還沒取消掉(compareAndSetWaitStatus(node, Node.CONDITION, 0)成功),那麼將其入隊同步隊列。在一定條件下(無法設置node前驅狀態爲SIGNAL時),還將喚醒node代表線程。然後處理下一個節點。

另外注意,signalAll方法直到結束返回,都一直沒有釋放鎖呢(因爲沒有在signalAll裏面執行過release),也就是說,執行signalAll的線程一直都是持有鎖的。

signal流程

相比signalAllsignal方法只會喚醒一個node,準確的說,是喚醒從同步隊列隊頭開始的第一個狀態爲CONDITION的node。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);  //與之前的doSignalAll不同,這裏是doSignal
}

首先看調用signal的線程是否已經擁有了獨佔鎖,不然就會拋出異常。與之前的doSignalAll不同,這裏調用的是doSignal

        private void doSignal(Node first) {//first參數是隊頭
            do {
            	// 更新隊頭,讓隊頭往後移動一位
                if ( (firstWaiter = first.nextWaiter) == null)
                	//如果發現新隊頭爲null,讓隊尾也爲null
                    lastWaiter = null;
                // 老套路,把參數和後面的鏈表斷開
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null); // transferForSignal失敗纔會執行這行
                     // 獲得新隊頭,如果新隊頭不爲null,則繼續下一次循環,因爲現在一次signal都沒有成功呢
        }
  • 整個邏輯是一個do while循環,不過這個循環一遍不會執行多次,只要有一次signal成功了(transferForSignal(first)返回了真)就不會繼續循環了,因爲這個函數的目的就只是signal一個節點。
  • 如果signal失敗了,那麼獲得新隊頭((first = firstWaiter) != null),只要新隊頭不爲null,則繼續下一次循環。
  • 相比signalAll流程,我們使用的還是這個transferForSignal來做的signal動作,但這裏我們終於使用到了transferForSignal的返回值。
  • 總之,signal方法會喚醒條件隊列中第一個狀態不是取消狀態(不是CONDITION)的節點。

transferForSignal假設不喚醒node線程

按照上面signal的流程(包括signalsignalAll),假設之前node線程沒有被中斷過,且執行signalAll的線程不喚醒node線程,那麼執行signal流程完畢後此時node的狀態爲:

  • 在數據結構上,node已經離開了條件隊列(first.nextWaiter = null),處於了同步隊列上了(Node p = enq(node))。
  • 在執行過程上,node線程當前還是阻塞在LockSupport.park(this)這裏。(這一點沒有變化)

要想node線程執行完await()方法,得需要執行unlock的線程出馬了。當node已經成爲了head後繼,且獲得獨佔鎖的線程開始執行unlock釋放鎖,將會喚醒node線程。node線程從LockSupport.park(this)處喚醒後,不會因爲有中斷狀態而break出循環(假設沒有被中斷過,先不用看checkInterruptWhileWaiting的實現,這裏只需要知道interruptMode還是會保持爲0就行),然後判斷循環條件!isOnSyncQueue(node)發現不成立而退出循環,然後將執行acquireQueued(node, savedState),但也不一定能獲得鎖,如果不能獲得,自然還是阻塞在acquireQueuedshouldParkAfterFailedAcquire裏。

transferForSignal假設喚醒node線程

按照上面signal的流程,假設之前node線程沒有被中斷過,且執行signalAll的線程喚醒node線程,那麼執行signal流程完畢後此時node的狀態爲:

  • 在數據結構上,node已經離開了條件隊列(first.nextWaiter = null),處於了同步隊列上了(Node p = enq(node))。
  • 在執行過程上,node線程從LockSupport.park(this)這裏被喚醒,不會因爲有中斷狀態而break出循環,然後判斷循環條件!isOnSyncQueue(node)發現不成立而退出循環,然後執行acquireQueued。如果不能獲得鎖,還是會阻塞在acquireQueuedshouldParkAfterFailedAcquire裏。

要想node線程執行完await()方法,還是得需要執行unlock的線程出馬。它執行unlock後,node線程從acquireQueuedshouldParkAfterFailedAcquire處被喚醒,然後再一次去獲得鎖。但也不一定能獲得鎖,如果不能獲得,自然還是阻塞在acquireQueuedshouldParkAfterFailedAcquire裏。

中斷流程(有線程將node線程中斷,在signal之前)

中斷await當前線程的線程終於出馬了(現在考慮沒有執行signal的線程,或者說中斷這個node在signal這個node之前)。假設之前node線程有被中斷過,且在signal之前,看看此時是怎樣的流程。首先要知道,中斷await當前線程的線程執行完中斷動作後,我們就不用關心它了,剩餘動作還是靠node線程自己完成的。

本場景下,中斷來臨之前,node的狀態就和 await()第一次調用park之後 一樣:

  • 在數據結構上,node在條件隊列上(addConditionWaiter)。
  • 在執行過程上,node線程當前阻塞在LockSupport.park(this)這裏。

首先,中斷來了以後,node線程會從LockSupport.park(this)處被喚醒,然後執行checkInterruptWhileWaiting(之前一直沒有講這個函數,是因爲在這個流程中它纔會真正發揮作用,之前的signal流程它肯定會返回0的,而返回就不會break出循環)。

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        
  • Thread.interrupted()首先判斷當前線程有沒有被中斷過,如果沒有,那麼返回0.
  • 如果有中斷過,那麼通過transferAfterCancelledWait(node)判斷返回THROW_IE還是REINTERRUPT.

checkInterruptWhileWaiting的返回值最終是會賦值給局部變量interruptMode的,它現在有三種可能值:

  1. 0:代表整個await過程中沒有發生過中斷。
  2. THROW_IE:代表await執行完畢返回用戶代碼處時,需要拋出異常。當中斷流程發生在signal流程之前時。
  3. REINTERRUPT:代表await執行完畢返回用戶代碼處時,不需要拋出異常,僅需要重新置上中斷狀態。當signal流程發生在中斷流程之前時。

之所以THROW_IEREINTERRUPT兩個值所代表的場景需要進行區分,是因爲一個線程A因await而進入condition queue後,正常的流程是另一個線程B執行signalsignalAll後才使得線程A的node入隊到sync queue。但如果中斷流程發生在signal流程之前,也能使得線程A的node入隊到sync queue,但這就沒有走正常的流程了。

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

首先通過CAS設置node狀態從CONDITION變成0,如果成功了,就將node入隊同步隊列,然後直接返回true。注意,這裏的CAS操作和transferForSignal裏的compareAndSetWaitStatus(node, Node.CONDITION, 0)是一樣的,也就是說:執行signal的線程 和 因中斷被喚醒的node線程,在這句compareAndSetWaitStatus(node, Node.CONDITION, 0)這裏有競爭關係,誰競爭贏了,誰纔有資格將node入隊同步隊列enq(node)

這樣的競爭關係是很必要的,直接避免兩個線程將同一個node重複入隊。

通過CAS設置node狀態從CONDITION變成0,如果失敗了,那就不能再執行enq(node)啦。因爲肯定有另一個signal線程正在執行enq(node)或者已經執行完了enq(node)了。

但這裏我們如果發現另一個signal線程還沒有執行完了enq(node)(通過!isOnSyncQueue(node)條件判斷),就必須一直等待,直到另一個signal線程執行完了enq(node),然後循環纔可以退出。之所以這麼等一下,是因爲如果不等,node線程自己接下來就會執行到acquireQueued了,而執行acquireQueued的前提就是已經入隊同步隊列完畢。

等到node線程執行完了checkInterruptWhileWaiting,考慮本文有中斷的場景,就會直接break出循環,然後執行到acquireQueued。如果不能獲得鎖,還是會阻塞在acquireQueuedshouldParkAfterFailedAcquire裏。

因中斷被喚醒的node線程 和 signal線程 的競爭關係

上面說了中斷流程和signal流程誰在前面,await的表現也會有所不同。具體的說,則體現在:因中斷被喚醒的node線程 和 signal線程 的競爭關係上。這兩個線程完全有可能同時在執行中,而它們的競爭點則體現在:

  • 因中斷被喚醒的node線程。transferAfterCancelledWait函數裏的compareAndSetWaitStatus(node, Node.CONDITION, 0)
  • signal線程。transferForSignal函數裏的compareAndSetWaitStatus(node, Node.CONDITION, 0)

這兩個transfer方法都會執行同一個CAS操作,但很明顯,只能有一個線程能夠執行CAS操作成功。

  • 競爭成功的那一方,transfer方法會返回true,並且會執行enq(node)
  • 競爭失敗的那一方,transfer方法會返回false,並且不會執行enq(node)
  • 當一個處於條件隊列上的node,狀態從CONDITION變成0時,就意味着它正在前往同步隊列,或者已經放置在同步隊列上了。
  • 如果transferAfterCancelledWait競爭成功,我們稱這個node線程走的是中斷流程。
  • 如果transferForSignal競爭成功,我們稱這個node線程走的是signal流程。

終於執行到acquireQueued

講了半天,終於講到acquireQueued了。但重點內容其實都在前面,acquireQueued後面的都是一些善後處理而已了。

既然已經執行到了acquireQueued,說明又會走獨佔鎖的獲取過程了,在此不贅述了。我們只需要知道,從acquireQueued返回時,node線程已經獲取到了鎖,並且返回了acquireQueued過程中是否有過中斷。注意,這和acquireQueued執行前發生的中斷是兩個不同的中斷,acquireQueued執行前發生的中斷會被checkInterruptWhileWaiting消耗掉,並賦值給interruptMode的。

        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;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  //已經執行到這裏啦!!!
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

注意,int savedState = fullyRelease(node)之前釋放的同步器狀態,現在acquireQueued(node, savedState)都要重新全部再獲取回來。

acquireQueued期間發生的中斷,重要性不如acquireQueued之前發生的中斷。假設acquireQueued發生了中斷,acquireQueued(node, savedState)則返回true,然後此時interruptMode有三種情況:

  1. interruptMode爲0,說明acquireQueued之前沒發生過中斷。interruptMode != THROW_IE判斷成功。所以需要將interruptMode升級爲REINTERRUPT
  2. interruptModeREINTERRUPT,說明acquireQueued之前發生過中斷(signal流程在中斷流程之前的那種)。interruptMode != THROW_IE判斷成功。然後將interruptModeREINTERRUPT變成REINTERRUPT,這好像是脫褲子放屁,但邏輯這樣寫就簡潔了。
  3. interruptModeTHROW_IE,說明acquireQueued之前發生過中斷(中斷流程在signal流程之前的那種)。interruptMode != THROW_IE判斷失敗。不會去執行interruptMode = REINTERRUPT,因爲執行了反而使得中斷等級下降了。說到底,還是因爲acquireQueued期間發生的中斷,重要性不如acquireQueued之前發生的中斷。
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();

如果發現node的條件隊列的nextWaiter還沒有斷開,則需要做一下善後處理。回想signal流程和中斷流程:

  • signal流程中(signalsignalAll方法),都會執行first.nextWaiter = null;的,所以如果node線程之前走的是signal流程,那這裏不會執行。
  • 中斷流程中,不會去執行first.nextWaiter = null;的,所以如果node線程之前走的是中斷流程,那這裏會執行。
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();  // 返回用戶代碼之前,自我中斷一下
        }

最後根據interruptMode來判斷做出不同的中斷反應。

await()總結

await()對於使用者來說,進入await()時是持有鎖的,阻塞後退出await()時也是持有鎖的。signal() signalAll()也是一樣。

從實現內部的持有鎖情況來看:

  • await()在從開頭到fullyRelease執行前,是持有鎖的。
  • await()在從fullyRelease執行後 到 acquireQueued執行前,是沒有持有鎖的。
  • await()acquireQueued執行後到最後,是持有鎖的。
  • signal() signalAll()全程都是持有鎖的。

await()的整體流程如下:

  1. 將當前線程包裝成一個node後(Node node = addConditionWaiter()),完全釋放鎖(int savedState = fullyRelease(node))。
  2. 當前線程阻塞在LockSupport.park(this)處,等待signal線程或者中斷線程的到來。
  3. 被喚醒後,到達acquireQueued之前,當前線程的node已經置於sync queue之上了。
  4. 執行acquireQueued,進行阻塞式的搶鎖。
  5. 退出acquireQueued時,當前線程已經重新獲得了鎖,之後進行善後工作。

awaitUninterruptibly()

前面介紹的await方法裏,中斷來臨時會使得當前線程離開while循環進而去執行acquireQueued開始搶鎖。換句話說,await方法允許,當前線程因爲中斷而不是因爲signal,而最終退出await方法(畢竟acquireQueued最終還是會搶鎖成功的)。

有時候我們希望,退出await方法的原因,只能是因爲signal,所以就需要使用awaitUninterruptibly了。

        public final void awaitUninterruptibly() {
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            boolean interrupted = false;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

首先看到awaitUninterruptibly不會拋出中斷異常,我們拿它與await方法進行下對比:

        public final void await() throws InterruptedException {
            if (Thread.interrupted())  // 不同之處1
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;  // 不同之處2
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)  // 不同之處3
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  // 不同之處4
                interruptMode = REINTERRUPT;
            // 後面的善後工作全都不需要做了
            if (node.nextWaiter != null)  // 不同之處5
                unlinkCancelledWaiters();
            if (interruptMode != 0)  // 不同之處6
                reportInterruptAfterWait(interruptMode);
        }

可以發現awaitUninterruptibly方法只是對await方法進行了大面積的刪改:

  1. 函數開頭不需要檢查調用前是否被中斷過了
  2. 用來記錄中斷狀態的變量只需要記錄兩種狀態(中斷過,或沒有中斷),所以用boolean變量就夠了。
  3. 在while循環裏,當前線程如果只是因爲中斷而被喚醒,那麼消耗掉中斷狀態(Thread.interrupted())。如果還沒有signal線程的到來,那麼當前線程的node還是不處於sync queue之上的,所以下次循環繼續,然後又阻塞在LockSupport.park(this)這裏。
  4. 如果搶鎖過程中(acquireQueued(node, savedState))發生過中斷,或者搶鎖之前發生過中斷(|| interrupted),那麼就自我中斷一下。
  5. 不需要判斷當前線程node在條件隊列上的鏈接是否斷開,因爲awaitUninterruptibly方法只會因爲signal流程會退出while循環,而signal流程肯定會 斷開條件隊列上的鏈接的。
  6. 不需要執行reportInterruptAfterWait了,因爲自我中斷已經做過了。

經過上面分析可以發現,awaitUninterruptibly方法全程都不會響應中斷,不管是在搶鎖過程之前還是之中發生過中斷,都是隻是簡單地自我中斷一下就好了。

而因爲awaitUninterruptibly方法不會去執行checkInterruptWhileWaiting,所以要想滿足退出while循環的條件!isOnSyncQueue(node)進而去執行acquireQueued開始搶鎖,只能是因爲signal流程中執行了transferForSignal(裏面執行了enq(node),使得node入隊了sync queue)。而從使用者的角度看,中斷並不能使得線程從await調用處喚醒,只有執行了signal,線程才能從await調用處喚醒。

總結一下awaitUninterruptibly方法:

  • 中斷會喚醒當前線程,但當前線程的node還是不處於sync queue之上,所以當前線程馬上又會阻塞。
  • 只有signal方法纔可以使得當前線程的node處於sync queue之上。
  • 調用該方法中,如果發生了中斷,會在返回用戶代碼之前,自我中斷一下。

awaitNanos(long nanosTimeout)

前面的方法不管是awaitawaitUninterruptibly,它們在while循環中如果一直沒有中斷線程或者signal線程的到來,會一直阻塞在while循環的park處。如果長時間signal線程一直不來,當前線程就會一直阻塞(一直阻塞就會一直不會去執行acquireQueued,也就不可能執行完函數了),所以此時我們可能需要一個帶有超時機制的awaitNanos(long nanosTimeout),如果超時了就啥也不用管,直接去執行acquireQueued

參數nanosTimeout,代表你最多願意在這個方法等待多長時間。
返回值long,代表nanostimeout值 減去 花費在等待在此方法上的時間 的估算。

        public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;  // 不同之處1
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {  // 不同之處2
                    transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)  // 不同之處3
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();  // 不同之處4
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();  // 不同之處5
        }

讓我們直接來看awaitNanos(long nanosTimeout)方法它與await()的不同之處:

  1. 需要計算出一個deadline,作爲是否超時的標準。
  2. 如果LockSupport.parkNanos(this, nanosTimeout)之後的這段長達nanosTimeout的時間段內,既沒有中斷來臨(不會進入if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)分支而break出循環),也沒有signal來臨(那麼當前線程node還是不處於sync queue上,循環條件!isOnSyncQueue(node)通過),線程足足阻塞了nanosTimeout這麼久才被喚醒,那麼經過nanosTimeout = deadline - System.nanoTime()重新計算後,就肯定會進入到if (nanosTimeout <= 0L)分支,執行transferAfterCancelledWait將node入隊同步隊列,然後退出循環,開開心心地去執行acquireQueued就好了。
  3. 只有當nanosTimeout >= spinForTimeoutThreshold,纔可以阻塞當前線程,不然時間太短的話,就直接自旋就好了。這是因爲考慮到阻塞線程和喚醒線程的過程,時間太短就不好控制了。注意進入這個分支的可能性:
    1. 用戶給的nanosTimeout太小,第一次進入循環時,就開始自旋。
    2. 走了signal流程,signal流程在一定條件下喚醒了當前線程。但喚醒時剩餘時間已經很少了。
    3. 走了signal流程,但沒有喚醒當前線程。之後當前線程node已經成爲了head後繼,然後另一個線程執行了unlock,喚醒了當前線程。但喚醒時剩餘時間已經很少了。
    4. 不可能是走的中斷流程。因爲會直接break出循環,也就不會執行這個分支。
  4. nanosTimeout = deadline - System.nanoTime()計算出剩餘時間還有多久。能執行到這裏,說明之前肯定沒有過中斷。
  5. return deadline - System.nanoTime()返回剩餘時間還有多久。

我們來總結下awaitNanos(long nanosTimeout)中能執行到acquireQueued的幾種流程:

  • 都已經超時了,且之前沒有過中斷,那麼接下來就會去執行acquireQueued,不過分兩種情況:
    • 如果之前signal線程來過,signal線程就已經把當前線程node放到同步隊列裏去了,所以!isOnSyncQueue(node)循環條件不成立,直接退出循環進而去執行的acquireQueued
    • 如果之前signal線程沒有來過,!isOnSyncQueue(node)循環條件成立,進入if (nanosTimeout <= 0L)分支去執行transferAfterCancelledWait讓當前線程node先入隊後,再去執行acquireQueued
  • 因爲來了中斷,而去執行的acquireQueued。這個過程和await()一樣。
  • 因爲signal流程中的一定條件的喚醒,或因爲執行unlock的線程而喚醒。這兩種情況,當前線程node都已經處於同步隊列上了,所以循環條件不成立而退出循環,進而去執行的acquireQueued
if (nanosTimeout <= 0L) {
    transferAfterCancelledWait(node);
    break;
}

當然,如果用戶給的參數nanosTimeout本來就是<=0的,第一次循環就會直接將當前線程node加入到同步隊列中,然後退出循環後進而執行acquireQueued

總結一下awaitNanos(long nanosTimeout):相比await()方法,它能在超時後,無條件地去執行acquireQueued,而這不需要signal線程或中斷線程的到來。

await(long time, TimeUnit unit)

理解了awaitNanos(long nanosTimeout),這個await(long time, TimeUnit unit)方法就好懂多了。從參數上就可以看出來,它只是對時間單位進行了拓展。我們直接看看它與awaitNanos(long nanosTimeout)的不同之處。

        public final boolean await(long time, TimeUnit unit)  //不同之處1
                throws InterruptedException {
            long nanosTimeout = unit.toNanos(time);  //不同之處2
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;
            boolean timedout = false;  //不同之處3
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    timedout = transferAfterCancelledWait(node);  //不同之處4
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return !timedout;  //不同之處5
        }

來分析一下不同之處:

  1. 返回值是boolean型。
  2. 將時間最終都轉換爲nanos的時間。
  3. 增加了一個局部變量timedout,代表退出while循環是否是因爲超時才退出的。退出循環後會接着執行acquireQueued
  4. 如果當前線程因爲超時而喚醒,會進入到if (nanosTimeout <= 0L)分支,之後如果transferAfterCancelledWait執行成功,會將局部變量timedout置爲true,代表退出while循環是因爲超時才退出的。
  5. 返回!timedout,所以返回值爲false,代表退出while循環是因爲超時才退出的;返回值爲true,代表退出while循環不是因爲超時才退出的。

可以發現,await(long time, TimeUnit unit)的整個過程與awaitNanos(long nanosTimeout)幾乎一樣,只是返回值類型不一樣了。但await(long time, TimeUnit unit)只關心退出while循環的原因,awaitNanos(long nanosTimeout)關心的是整個執行過程中花費的時間。

所以,並不能簡單的認爲,調用await(long time, TimeUnit unit) 等價於 調用awaitNanos(unit.toNanos(time)) > 0。比如考慮這種場景,假設沒有中斷,退出循環是因爲signal線程來過才退出的循環,但直到執行unlock的線程來喚醒當前線程進而使得當前線程得到鎖卻很遲。此時:

  • awaitNanos(unit.toNanos(time))返回的值是小於0的,所以awaitNanos(unit.toNanos(time)) > 0返回false。
  • await(long time, TimeUnit unit)退出while循環之前,不會去執行timedout = transferAfterCancelledWait(node),因爲是直接不滿足了循環條件!isOnSyncQueue(node)。所以timedout = false,返回!timedout,即返回的是true了。
  • 按照上面兩點,發現二者不一致了。

awaitUntil(Date deadline)

這個方法其實和await(long time, TimeUnit unit)幾乎一模一樣,只是獲得deadline的方式改變了而已,以前是自己計算出來。

        public final boolean awaitUntil(Date deadline)
                throws InterruptedException {
            long abstime = deadline.getTime();  //不同之處1
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            // 此處不需要計算出deadline了,因爲參數給了。  不同之處2
            boolean timedout = false;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (System.currentTimeMillis() > abstime) {  //不同之處3
                    timedout = transferAfterCancelledWait(node);
                    break;
                }
                LockSupport.parkUntil(this, abstime);  //不同之處4
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return !timedout;
        }

分析一下不同之處:

  1. long abstime = deadline.getTime(),直接獲得這個絕對時間。
  2. 此處不需要計算出deadline了,因爲參數給了。
  3. 以前是判斷 deadline與當前系統時間 之間的差值。現在是比較 deadline與當前系統時間 之間的大小。
  4. 相比之前兩個超時版本,這裏沒有使用自旋優化,在剩餘時間特別短的時候。所以調用這個方法時,最好給定的絕對時間比較遠,才比較好。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章