高併發學習之13Condition的實現原理分析

1. 簡介

任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()
wait(long timeout)notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。
Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock對象的,而且在AQS中我們也提到AQS(同步器) 中使用Condition存放等待線程。

2. Condition使用

還是老規矩,寫事例前先看下Condition接口源碼爲我們定義了哪些api。

public interface Condition {

	//當前線程進入等待狀態直到被通知(signal)或中斷。
	//當前線程將進入運行狀態且從 awaitO 方法返回的情況,包括:
	//其他線程調用該 Condition 的 signl() 或 signalAll()方法,而當前線程被選中喚醒
	//其他線程(調用 interrupt(方法)中斷當前線程
	//如果當前等待線程從 await()方法返回,那麼表明該線程已經獲取了 Condition對象所對應的鎖
    void await() throws InterruptedException;
    
	//當前線程進入等待狀態直到被通知,從方法名稱上可以看出該方法對中斷不敏感
    void awaitUninterruptibly();
    
	//當前線程進入等待狀態直到被通知、中斷或者超時。返回值表示剩餘的時間
	//如果在 nanosTimeout納秒之前被喚醒,那麼返回值就是(nanosTimeout-實際耗時)
	//如果返回值是0 或者負數,那麼可以認定已經超時了
    long awaitNanos(long nanosTimeout) throws InterruptedException;
	
	//指定等待時間,直到超時返回false
    boolean await(long time, TimeUnit unit) throws InterruptedException;
	
	//當前線程進入等待狀態直到被通知、中斷或者到某個時間。
	//如果沒有到指定時間就被通知,方法返回true,否則,表示到了指定時間,方法返回 false
    boolean awaitUntil(Date var1) throws InterruptedException;
	
	//喚醒 一個等待在 Condition 上的線程,該線程從等待方法返回前必須獲得與Condition 相關聯的鎖
    void signal();
	
	//喚醒所有等待在Condition上的線程, 能夠從等待方法返回的線程必須獲得與Condition 相關聯的鎖
    void signalAll();
}

Condition的使用方式比較簡單,需要注意在調用方法前獲取鎖,使用方式如下:

Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }
    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

如示例所示,一般都會將Condition對象作爲成員變量。當調用await()方法後,當前線程會釋放鎖並在此等待,而其他線程調用Condition對象的signal()方法,通知當前線程後,當前線程才從await()方法返回,並且在返回前已經獲取了鎖。
獲取一個Condition 必須通過Lock的 newCondition() 方法。下面通過一個有界隊列的示例來深入瞭解Condition的使用方式。有界隊列是一種特殊的隊列,當隊列爲空時,隊列的獲取操作將會阻塞獲取線程,直到隊列中有新增元素,當隊列已滿時,隊列的插入操作將會阻塞插入線程,直到隊列出現“空位”,其代碼如下:

public class BoundedQueue<T> {

    private Object[] items;
    // 添加的下標,刪除的下標和數組當前數量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size) {
        items = new Object[size];
    }
    // 添加一個元素,如果數組滿,則添加線程進入等待狀態,直到有"空位"
    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[addIndex] = t;
            if (++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    // 由頭部刪除一個元素,如果數組空,則刪除線程進入等待狀態,直到有新添加元素
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

上述示例中,BoundedQueue通過add(T t)方法添加一個元素,通過remove()方法移出一個元素。以添加方法爲例:

  • 首先需要獲得鎖,目的是確保數組修改的可見性和排他性。
  • 當數組數量等於數組長度時,表示數組已滿,則調用notFull.await(),當前線程隨之釋放鎖並進入等待狀態。
  • 如果數組數量不等於數組長度,表示數組未滿,則添加元素到數組中,同時通知等待在notEmpty上的線程,數組中已經有新元素可以獲取。
    在添加和刪除方法中使用while循環而非if判斷,目的是防止過早或意外的通知,只有條件符合才能夠退出循環。回想之前提到的等待/通知的經典範式,二者是非常類似的。

3. Condition實現分析

在 Condition newCondition() 方法中,返回一個 ConditionObject,ConditionObject是同步器AbstractQueuedSynchronizer的內部類,因爲Condition的操作需要獲取相關聯的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含着一個隊列(以下稱爲等待隊列),該隊列是Condition對象實現等待/通知功能的關鍵。
下面將分析Condition的實現,主要包括:等待隊列等待通知,下面提到的Condition如果不加說明均指的是ConditionObject。

3.1 等待隊列

等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程將會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類AbstractQueuedSynchronizer.Node
一個Condition包含一個等待隊列,Condition擁有首節點(firstWaiter)和尾節點(lastWaiter)。
其源碼如下:

public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
        ....
}

當前線程調用Condition.await()方法,將會以當前線程構造節點,並將節點從尾部加入等待隊列,等待隊列的基本結構如圖所示:
等待隊列的基本結構
如圖所示,Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。上述節點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。
同時還有一點需要注意的是:我們可以多次調用lock.newCondition()方法創建多個condition對象,也就是一個lock可以持有多個等待隊列。而在之前在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而併發包中的Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列,其對應關係如圖:同步隊列與等待隊列
如圖所示,Condition的實現是同步器的內部類,因此每個Condition實例都能夠訪問同步器提供的方法,相當於每個Condition都擁有所屬同步器的引用。

3.2 等待

調用Condition的await()方法(或者以await開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從await()方法返回時,當前線程一定獲取了Condition相關聯的鎖。
如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。
Condition的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;
	}
	if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
		interruptMode = REINTERRUPT;
	if (node.nextWaiter != null)
		unlinkCancelledWaiters();
	if (interruptMode != 0)
		reportInterruptAfterWait(interruptMode);
}

調用該方法的線程成功獲取了鎖的線程,也就是同步隊列中的首節點,該方法會將當前線程構造成節點並加入等待隊列中,然後釋放同步狀態,喚醒同步隊列中的後繼節點,然後當前線程會進入等待狀態。

當等待隊列中的節點被喚醒,則喚醒節點的線程開始嘗試獲取同步狀態。如果不是通過其他線程調用Condition.signal()方法喚醒,而是對等待線程進行中斷,則會拋出InterruptedException。
如果從隊列的角度去看,當前線程加入Condition的等待隊列,該過程如圖
當前線程加入等待隊列
如圖所示,同步隊列的首節點並不會直接加入等待隊列,而是通過**addConditionWaiter()**方法把當前線程構造成一個新的節點並將其加入等待隊列中。

3.3 通知

調用Condition的**signal()**方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。
Condition的signal()方法,如下:

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

調用該方法的前置條件是當前線程必須獲取了鎖,可以看到signal()方法進行了isHeldExclusively()檢查,也就是當前線程必須是獲取了鎖的線程。接着獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。
節點從等待隊列移動到同步隊列的過程如圖所示
節點從等待隊列移動到同步隊列
通過調用同步器的 enq(Node node) 方法,等待隊列中的頭節點線程安全地移動到同步隊列。當節點移動到同步隊列後,當前線程再使用LockSupport喚醒該節點的線程。
被喚醒後的線程,將從await()方法中的while循環中退出(isOnSyncQueue(Node node)方法返回true,節點已經在同步隊列中),進而調用同步器的**acquireQueued()方法加入到獲取同步狀態的競爭中。
成功獲取同步狀態(或者說鎖)之後,被喚醒的線程將從先前調用的await()方法返回,此時該線程已經成功地獲取了鎖。
Condition的
signalAll()**方法,相當於對等待隊列中的每個節點均執行一次signal()方法,效果就是將等待隊列中所有節點全部移動到同步隊列中,並喚醒每個節點的線程。

4. 總結

  • Condition是配合Lock實現等待/喚起機制
  • ConditionObject 是AQS內部類,相當於其是AQS實現的一部分。
  • ConditionObject 也是鏈表結構,有 firstWaiter(首節點)和lastWaiter(尾節點)
  • ConditionObject 使用的節點結構和AQS中同步隊列中是同一結構
  • ConditionObject 在AQS語義上來看是等待隊列,通過await()方法從等待隊列移入到同步隊列,這樣纔能有機會執行完成代碼
  • AQS中如果獲取同步狀態失敗,則會從同步隊列移入到等待隊列,只要直到其它節點調用signal()方法,節點被喚醒移入到同步隊列中。

其實只要清楚Condition是鏈表結構,是用來配合lock,實現等待喚醒功能,其次還要直到在AQS中,其是等待隊列,在其次就是等待隊列和同步隊列節點是怎麼切換的。

5. 補充一個例子

下面附一個小例子:

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(new waiter());
        waiter.start();
        Thread signaler = new Thread(new signaler());
        signaler.start();
    }

    static class waiter implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "當前條件不滿足等待");
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知條件滿足");
            } finally {
                lock.unlock();
            }
        }
    }

    static class signaler implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
輸出結果:

Thread-0當前條件不滿足等待
Thread-0接收到通知,條件滿足

參考文獻 《Java併發編程的藝術》

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